Area: sdk — performance / DX · found while migrating a browser app onto the SDK
Expected: a bodyless GET from a browser is a CORS "simple request" and goes straight out — no preflight.
Actual: every request carries a Content-Type, including reads with no body. clients/ts/src/http.ts:65:
const headers: Record<string, string> = {
"Content-Type": opts.contentType ?? "application/json",
Accept: "application/json",
};
There is no method or body check — the header is set for schema.get(), pipes.list(), policy.get(), a plain .select(), everything. application/json is not one of the three CORS-safelisted Content-Type values (application/x-www-form-urlencoded, multipart/form-data, text/plain), so a cross-origin read costs an OPTIONS round trip before the GET that actually fetches data. (Accept: application/json is fine — that one is safelisted.)
Why it matters: the standard browser deployment is the SDK on one origin and WaveHouse on another (app.example.com → wh.example.com, or any subdomain split), and that is exactly the shape where this doubles the request count for reads. Access-Control-Max-Age bounds the cost but doesn't remove it — browsers cap the value (Chrome at 2h, Firefox at 24h), it's per method+URL, and the first load of every screen — the one users actually feel — pays it regardless.
It's also just wrong on HTTP grounds: a Content-Type describes a representation in the message body, so on a request with no body there is nothing for it to describe.
No client-side workaround. options.headers is applied underneath the SDK's own headers (deliberately, and documented — auth owns Authorization, and a request's Content-Type/Accept can't be displaced), so a consumer cannot unset it. options.fetch could strip it, but replacing the entire HTTP implementation to remove one header is not a fix anyone should have to reach for.
Scope: set Content-Type only when the request actually has a body — i.e. when opts.body or opts.rawBody is defined. contentType overrides keep working for the bodies that need them (NDJSON ingest). Accept stays unconditionally, since it's safelisted and costs nothing.
Worth a test asserting a bodyless GET carries no Content-Type, since the regression is invisible in any same-origin or server-side test — it only shows up as a preflight in a browser against a second origin.
Found while migrating a first-party web console onto the SDK, 2026-08-13.
Area: sdk — performance / DX · found while migrating a browser app onto the SDK
Expected: a bodyless
GETfrom a browser is a CORS "simple request" and goes straight out — no preflight.Actual: every request carries a
Content-Type, including reads with no body.clients/ts/src/http.ts:65:There is no method or body check — the header is set for
schema.get(),pipes.list(),policy.get(), a plain.select(), everything.application/jsonis not one of the three CORS-safelistedContent-Typevalues (application/x-www-form-urlencoded,multipart/form-data,text/plain), so a cross-origin read costs anOPTIONSround trip before theGETthat actually fetches data. (Accept: application/jsonis fine — that one is safelisted.)Why it matters: the standard browser deployment is the SDK on one origin and WaveHouse on another (
app.example.com→wh.example.com, or any subdomain split), and that is exactly the shape where this doubles the request count for reads.Access-Control-Max-Agebounds the cost but doesn't remove it — browsers cap the value (Chrome at 2h, Firefox at 24h), it's per method+URL, and the first load of every screen — the one users actually feel — pays it regardless.It's also just wrong on HTTP grounds: a
Content-Typedescribes a representation in the message body, so on a request with no body there is nothing for it to describe.No client-side workaround.
options.headersis applied underneath the SDK's own headers (deliberately, and documented —authownsAuthorization, and a request'sContent-Type/Acceptcan't be displaced), so a consumer cannot unset it.options.fetchcould strip it, but replacing the entire HTTP implementation to remove one header is not a fix anyone should have to reach for.Scope: set
Content-Typeonly when the request actually has a body — i.e. whenopts.bodyoropts.rawBodyis defined.contentTypeoverrides keep working for the bodies that need them (NDJSON ingest).Acceptstays unconditionally, since it's safelisted and costs nothing.Worth a test asserting a bodyless
GETcarries noContent-Type, since the regression is invisible in any same-origin or server-side test — it only shows up as a preflight in a browser against a second origin.Found while migrating a first-party web console onto the SDK, 2026-08-13.