mirror of
https://github.com/hanzoai/gateway.git
synced 2026-10-30 15:04:44 +00:00
Two literal lists of the allowed request headers — one here, one in cloud's middleware_edge.go — and this one had drifted four names behind: the console's X-Actor-Id, X-Act-As-Project, X-Act-As-Org and X-CSRF-Token were added there and never here. That is what a second literal list of the same thing always does. A name absent from the list fails PREFLIGHT, and the browser reports that as an opaque "TypeError: Failed to fetch" with nothing on the server side to see. The request never arrives, so there is no log, no metric and no 4xx to notice. The same gap cost the billing checkout its X-Idempotency-Key on POST /v1/billing/subscribe/card, which meant no paid subscription could leave the browser at all. So stop maintaining the list. Echo the Access-Control-Request-Headers the browser is asking about — the Fetch-standard way to say "all of them", and the only answer that cannot go stale or drift from cloud's. Only well-formed tokens are echoed, so a hand-written ask cannot fold a CRLF into a response header. negotiation now runs only on the preflight, which is what its own comment says it is for. An ordinary response was carrying allow-headers and max-age that a browser ignores, and it is also the only way Vary can name the request-headers on the preflight without claiming an ordinary response depends on them.
51 lines
2.1 KiB
Go
51 lines
2.1 KiB
Go
// Copyright © 2026 Hanzo AI. MIT License.
|
|
|
|
// Untagged on purpose: [corsAllowHeaders] is a framework-free value, so it is
|
|
// exercised under BOTH builds. The transport suites around it are tagged because
|
|
// each drives one edge.
|
|
|
|
package gateway
|
|
|
|
import "testing"
|
|
|
|
// TestCORSAllowHeaders_AnswersTheAsk is the regression for the defect a literal
|
|
// allow-headers list guarantees: a client attaches a header nobody added, the
|
|
// preflight does not name it, and the browser kills the request with an opaque
|
|
// "TypeError: Failed to fetch" that never reaches a log.
|
|
//
|
|
// This copy had drifted four names behind cloud's, which is what a second literal
|
|
// list of the same thing always does. The names below are the ones it was missing
|
|
// — the console's four, and the X-Idempotency-Key the billing checkout stamps on
|
|
// POST /v1/billing/subscribe/card. Any header must work; that is the point.
|
|
func TestCORSAllowHeaders_AnswersTheAsk(t *testing.T) {
|
|
for _, ask := range []string{
|
|
"content-type, x-idempotency-key",
|
|
"x-actor-id, x-act-as-project, x-act-as-org, x-csrf-token",
|
|
"x-something-nobody-has-added-yet",
|
|
} {
|
|
if got := corsAllowHeaders(ask); got != ask {
|
|
t.Errorf("corsAllowHeaders(%q) = %q — the browser could not send what it asked about", ask, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestCORSAllowHeaders_EchoesTokensOnly: the echo is a header VALUE, so a
|
|
// hand-written ask must not be able to fold anything into the response.
|
|
func TestCORSAllowHeaders_EchoesTokensOnly(t *testing.T) {
|
|
cases := []struct{ ask, want string }{
|
|
{"", ""},
|
|
{"Content-Type", "Content-Type"},
|
|
{" x-a , x-b ", "x-a, x-b"},
|
|
{"x-a,,x-b", "x-a, x-b"}, // an empty name is not a name
|
|
{"x-ok\r\nX-Injected: 1", ""}, // CRLF makes the WHOLE name malformed
|
|
{"x-ok, x-bad\r\nX-Injected: 1", "x-ok"}, // …and only the malformed one is dropped
|
|
{"x-ok, bad header", "x-ok"}, // a space is not a tchar
|
|
{"x-ok, \"quoted\"", "x-ok"}, // nor is a quote
|
|
}
|
|
for _, tc := range cases {
|
|
if got := corsAllowHeaders(tc.ask); got != tc.want {
|
|
t.Errorf("corsAllowHeaders(%q) = %q, want %q", tc.ask, got, tc.want)
|
|
}
|
|
}
|
|
}
|