Expose API headers across CORS boundary

Cross-origin dashboards could not read x-ratelimit-*, x-total-count,
x-max-seq or x-truncated, which silently disabled the budget meter and
live-feed cursor backfill on split deployments.
This commit is contained in:
lakshit verma 2026-08-26 21:29:59 +05:30
parent b56447f067
commit a5d2e56f06
No known key found for this signature in database
3 changed files with 10 additions and 0 deletions

View file

@ -4,6 +4,11 @@ import (
"net/http" "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 // corsMiddleware answers with CORS headers when the request Origin is in
// the allow list, so a separately hosted dashboard (e.g. on Vercel) can // 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 // 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 { if origin == o {
h := w.Header() h := w.Header()
h.Set("Access-Control-Allow-Origin", origin) h.Set("Access-Control-Allow-Origin", origin)
h.Set("Access-Control-Expose-Headers", exposedHeaders)
h.Add("Vary", "Origin") h.Add("Vary", "Origin")
break break
} }

View file

@ -237,6 +237,9 @@ func TestCORS(t *testing.T) {
if v := rec.Header().Get("Vary"); !strings.Contains(v, "Origin") { if v := rec.Header().Get("Vary"); !strings.Contains(v, "Origin") {
t.Errorf("Vary = %q", v) 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 := httptest.NewRequest("GET", "/v1/stats", nil)
bad.Host = "localhost" bad.Host = "localhost"

1
internal/web/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.vercel