diff --git a/internal/server/cors.go b/internal/server/cors.go index 035db0a..aefade0 100644 --- a/internal/server/cors.go +++ b/internal/server/cors.go @@ -4,6 +4,11 @@ import ( "net/http" ) +// exposedHeaders lists the API headers the browser may read on +// cross-origin responses; by default only Cache-Control, Content-Type +// and friends cross the boundary. +const exposedHeaders = "x-ratelimit-limit, x-ratelimit-remaining, retry-after, x-total-count, x-max-seq, x-truncated" + // corsMiddleware answers with CORS headers when the request Origin is in // the allow list, so a separately hosted dashboard (e.g. on Vercel) can // call the API. The Origin is echoed back rather than wildcarded, and @@ -21,6 +26,7 @@ func corsMiddleware(allowed []string, next http.Handler) http.Handler { if origin == o { h := w.Header() h.Set("Access-Control-Allow-Origin", origin) + h.Set("Access-Control-Expose-Headers", exposedHeaders) h.Add("Vary", "Origin") break } diff --git a/internal/server/server_test.go b/internal/server/server_test.go index 229df21..d59a725 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -237,6 +237,9 @@ func TestCORS(t *testing.T) { if v := rec.Header().Get("Vary"); !strings.Contains(v, "Origin") { t.Errorf("Vary = %q", v) } + if ex := rec.Header().Get("Access-Control-Expose-Headers"); !strings.Contains(ex, "x-ratelimit-limit") || !strings.Contains(ex, "x-max-seq") { + t.Errorf("expose-headers = %q", ex) + } bad := httptest.NewRequest("GET", "/v1/stats", nil) bad.Host = "localhost" diff --git a/internal/web/.gitignore b/internal/web/.gitignore new file mode 100644 index 0000000..e985853 --- /dev/null +++ b/internal/web/.gitignore @@ -0,0 +1 @@ +.vercel