Summary
Two adjacent gaps in the core transport's HTTP-method coverage, surfaced together because both are "modern request shapes the engine doesn't fully understand":
QUERY — the new safe/idempotent/cacheable-with-a-body method (draft-ietf-httpbis-safe-method-w-body) works by accident but isn't a first-class citizen: the engine still treats "anything ≠ GET/HEAD" as an unsafe write.
- Beacon /
keepalive — there is no way to make an unload-safe (navigator.sendBeacon-style) request. The fetch adapter never sets keepalive, so page-unload telemetry is cancelled mid-flight.
They're related but independently shippable — happy to split into two issues if preferred.
Part 1 — First-class QUERY method
QUERY is a safe, idempotent, cacheable method that carries a request body (think "GET with a body" for complex/large queries that don't fit in a URL).
What already works ✅
cfg.method is a free string (packages/core/src/types.ts:198), so method: 'QUERY' is accepted — not rejected.
encodeRequestBody only drops the body for GET/HEAD (packages/core/src/http-adapter.ts:324), so a QUERY body is sent.
- The cache key already folds the request body into
canonicalRequest (packages/core/src/cache.ts:140), so opt-in caching (cache: { methods: ['QUERY'] }) would key correctly on the query payload — two different QUERY bodies to the same URL won't collide.
Gaps
-
Idempotency mis-classification. applyIdempotency short-circuits only GET/HEAD and treats everything else as a write, injecting an Idempotency-Key:
// packages/core/src/engine.ts:161
if (method === 'GET' || method === 'HEAD') return; // writes only
QUERY is safe — stamping it with an idempotency key is semantically wrong (only fires when idempotency is configured, so low blast radius, but it's the tell that the engine's notion of "safe method" is hard-coded to GET/HEAD).
-
Not cacheable by default. Cache defaults to ['GET','HEAD'] (packages/core/src/cache.ts:359, documented packages/core/src/types.ts:404), even though QUERY responses are spec-cacheable. Requiring explicit opt-in is defensible, but it should at least be documented that QUERY is a valid cache.methods entry and keys on the body.
-
No shared notion of method safety. GET/HEAD is special-cased inline in at least three places (idempotency engine.ts:161, body-stripping http-adapter.ts:324, redirect handling http-adapter.ts:243). There's no single isSafe(method) / isIdempotent(method) helper, so adding QUERY (and future methods) means hunting every site.
-
DX / discoverability. method: string gives no autocomplete or guidance; no docs page mentions QUERY.
Proposal
- Introduce a tiny internal method-classification helper (
isSafeMethod / isIdempotentMethod) and route the GET/HEAD special-cases through it, adding QUERY to the safe+idempotent set.
- Skip idempotency-key injection for safe methods (GET/HEAD/QUERY).
- Document
QUERY as a supported method and a valid cache.methods entry (note: it keys on the request body).
- (Optional) widen the
method type to a KnownMethod | (string & {}) union so common methods autocomplete without closing the door on custom verbs.
Acceptance criteria
Part 2 — Beacon / keepalive (unload-safe) requests
navigator.sendBeacon(url, data) ≡ fetch(url, { keepalive: true }): a fire-and-forget request the browser is obligated to complete even as the page unloads (pagehide / visibilitychange: hidden). This is the standard mechanism for exit telemetry.
Gap
The fetch adapter's RequestInit never sets keepalive:
// packages/core/src/http-adapter.ts:53
const init: FetchInitWithDispatcher = compact({
method,
headers,
body,
signal: req.signal,
dispatcher: opts?.dispatcher,
redirect: 'manual',
// no keepalive
});
And there's no field to request it — neither AdapterRequest (packages/core/src/types.ts:196) nor the per-call StitchInput (packages/core/src/types.ts:23) carries keepalive.
Impact: any stitch fired on page unload — third-party analytics, and the library's own tracing/otlp sinks — is aborted mid-flight when the user navigates away, silently dropping the last (often most important) event.
Proposal
- Add an opt-in
keepalive?: boolean on AdapterRequest and a way to set it (config-level on the stitch and/or per-call on StitchInput), threaded through buildRequest → adapter, and passed into the fetch init.
- Document the constraints so it's not mistaken for a free "always on" switch:
fetch + keepalive has a 64 KiB total request-body cap (across all in-flight keepalive requests).
- It can't stream the request body → mutually exclusive with upload streaming.
- Only the
fetch transport honors it; buffered/other adapters (axios, xhr) should ignore or reject it.
- Because of the body cap + fire-and-forget nature,
keepalive is per-stitch/per-call opt-in, never a default.
- (Nice-to-have) a small recipe/example: a telemetry stitch flushed on
visibilitychange with keepalive: true.
Acceptance criteria
Notes
- No existing issue covers either gap (checked open + closed for
QUERY method / keepalive / beacon).
- File references are against
main at the time of filing; both areas are pure core-transport changes (no new deps), consistent with the zero-dep / browser-first bars.
Summary
Two adjacent gaps in the core transport's HTTP-method coverage, surfaced together because both are "modern request shapes the engine doesn't fully understand":
QUERY— the new safe/idempotent/cacheable-with-a-body method (draft-ietf-httpbis-safe-method-w-body) works by accident but isn't a first-class citizen: the engine still treats "anything ≠ GET/HEAD" as an unsafe write.keepalive— there is no way to make an unload-safe (navigator.sendBeacon-style) request. The fetch adapter never setskeepalive, so page-unload telemetry is cancelled mid-flight.They're related but independently shippable — happy to split into two issues if preferred.
Part 1 — First-class
QUERYmethodQUERYis a safe, idempotent, cacheable method that carries a request body (think "GET with a body" for complex/large queries that don't fit in a URL).What already works ✅
cfg.methodis a freestring(packages/core/src/types.ts:198), somethod: 'QUERY'is accepted — not rejected.encodeRequestBodyonly drops the body forGET/HEAD(packages/core/src/http-adapter.ts:324), so aQUERYbody is sent.canonicalRequest(packages/core/src/cache.ts:140), so opt-in caching (cache: { methods: ['QUERY'] }) would key correctly on the query payload — two differentQUERYbodies to the same URL won't collide.Gaps
Idempotency mis-classification.
applyIdempotencyshort-circuits onlyGET/HEADand treats everything else as a write, injecting anIdempotency-Key:QUERYis safe — stamping it with an idempotency key is semantically wrong (only fires whenidempotencyis configured, so low blast radius, but it's the tell that the engine's notion of "safe method" is hard-coded to GET/HEAD).Not cacheable by default. Cache defaults to
['GET','HEAD'](packages/core/src/cache.ts:359, documentedpackages/core/src/types.ts:404), even thoughQUERYresponses are spec-cacheable. Requiring explicit opt-in is defensible, but it should at least be documented thatQUERYis a validcache.methodsentry and keys on the body.No shared notion of method safety. GET/HEAD is special-cased inline in at least three places (idempotency
engine.ts:161, body-strippinghttp-adapter.ts:324, redirect handlinghttp-adapter.ts:243). There's no singleisSafe(method)/isIdempotent(method)helper, so addingQUERY(and future methods) means hunting every site.DX / discoverability.
method: stringgives no autocomplete or guidance; no docs page mentionsQUERY.Proposal
isSafeMethod/isIdempotentMethod) and route the GET/HEAD special-cases through it, addingQUERYto the safe+idempotent set.QUERYas a supported method and a validcache.methodsentry (note: it keys on the request body).methodtype to aKnownMethod | (string & {})union so common methods autocomplete without closing the door on custom verbs.Acceptance criteria
method: 'QUERY'with a body sends the body (regression-guard the existing behavior).idempotencyconfigured, aQUERYcall does not get anIdempotency-Key.cache: { methods: ['QUERY'] }caches responses keyed on the request body; distinct bodies don't collide.QUERY(method support + caching note).Part 2 — Beacon /
keepalive(unload-safe) requestsnavigator.sendBeacon(url, data)≡fetch(url, { keepalive: true }): a fire-and-forget request the browser is obligated to complete even as the page unloads (pagehide/visibilitychange: hidden). This is the standard mechanism for exit telemetry.Gap
The fetch adapter's
RequestInitnever setskeepalive:And there's no field to request it — neither
AdapterRequest(packages/core/src/types.ts:196) nor the per-callStitchInput(packages/core/src/types.ts:23) carrieskeepalive.Impact: any stitch fired on page unload — third-party analytics, and the library's own tracing/otlp sinks — is aborted mid-flight when the user navigates away, silently dropping the last (often most important) event.
Proposal
keepalive?: booleanonAdapterRequestand a way to set it (config-level on the stitch and/or per-call onStitchInput), threaded throughbuildRequest→ adapter, and passed into the fetchinit.fetch+keepalivehas a 64 KiB total request-body cap (across all in-flight keepalive requests).fetchtransport honors it; buffered/other adapters (axios, xhr) should ignore or reject it.keepaliveis per-stitch/per-call opt-in, never a default.visibilitychangewithkeepalive: true.Acceptance criteria
keepalive, and the fetch adapter setskeepalive: trueon the underlyingfetchinit.Notes
QUERY method/keepalive/beacon).mainat the time of filing; both areas are pure core-transport changes (no new deps), consistent with the zero-dep / browser-first bars.