Skip to content

feat: unify the HTTP request contract across backends - #28

Merged
oboard merged 2 commits into
mainfrom
feat/unified-request-contract
Aug 21, 2026
Merged

feat: unify the HTTP request contract across backends#28
oboard merged 2 commits into
mainfrom
feat/unified-request-contract

Conversation

@oboard

@oboard oboard commented Aug 21, 2026

Copy link
Copy Markdown
Owner

Problem

The three HTTP backends (native, JS/Node, mongoose C) produced materially different HttpRequest values for the same request, so application behavior depended on which backend a deployment used:

  • Query strings — native stripped ?query before dispatch, so the handler could not recover it; JS routed against the raw req.url (path + query), so GET /hello?x=1 returned 404 against a registered GET /hello route.
  • PUT/PATCH bodies — native read bodies for POST/PUT/PATCH and framed requests; JS only read a body when the method was exactly POST, so PUT/PATCH reached handlers with an empty raw_body.
  • Header casing — headers were a case-sensitive Map[StringView, StringView]. Node lowercases incoming keys while native preserves wire casing, so event.req.headers.get("X-Test") behaved differently per backend even though HTTP field names are case-insensitive (per RFC 9110 §5.1).

Additionally, the JS adapter did its own inline dispatch rather than reusing the shared dispatch_http, so the JS serve path was not covered by in-process dispatch tests.

What changed

Define one backend-independent request contract:

  • Path + query separationdispatch_http now splits the request-target into a pure path (used for route matching and static-asset resolution) and a separately-retained HttpRequest.query. A ?query can no longer 404 a registered route; the handler can recover it.
  • Shared request_has_body predicate — drives body reading identically for the native and JS adapters: always for POST/PUT/PATCH, or when a body is framed via content-length / transfer-encoding.
  • Case-insensitive headersHttpRequest.headers and HttpResponse.headers are now to-owned, case-insensitive Map[@http.CaseInsensitiveString, StringView]. Because CaseInsensitiveString is implicitly convertible from String/string literals, existing call sites (map literals, .get/.set/[...]) compile unchanged while lookups become case-insensitive. The mongoose adapter's parse_headers returns the same type.
  • JS adapter now funnels through the shared dispatch_http — reusing shared routing, body reading, and error handling, so the JS serve path conforms to the same contract.

Conformance suite

request_conformance.mbt adds 6 tests that run on both the +js and +native targets (the workspace compiles/runs dispatch_http tests per-target). They assert:

  • Query strings do not break path routing.
  • Path and query are exposed separately.
  • Fragments are stripped from the query.
  • A query-free request yields an empty query string.
  • PUT and PATCH bodies reach the handler (body-method parity).
  • Request headers are case-insensitive.

Verification

  • moon check clean on both +js and +native targets.
  • moon test passes on both targets (67 native, 73 js).
  • Live smoke test — a small server with a GET route and a PUT echo handler, run twice:
Request native js
GET /hello/moonbit?x=1&y=2 path=/hello/moonbit query=x=1&y=2 path=/hello/moonbit query=x=1&y=2
PUT /echo/42 body + x-test: abc (then X-Test: MIXED, X-TEST: CAPS) body=payload-body header=abc body=payload-body header=abc (then MIXED, CAPS)

Identical on both backends.

🤖 Generated with Claude Code

oboard and others added 2 commits August 21, 2026 16:33
The three HTTP backends (native, JS, mongoose C) previously produced
materially different HttpRequest values for the same request, so app
behavior depended on which backend a deployment used:

- Query strings: native stripped ?query before dispatch (the handler
  could not recover it); JS routed against the raw req.url (path+query),
  so GET /hello?x=1 404'd a registered GET /hello route.
- Bodies: native read bodies for POST/PUT/PATCH and framed requests; JS
  read a body only when the method was exactly POST, so PUT/PATCH
  reached handlers with an empty raw_body.
- Headers: headers were a case-sensitive Map[StringView, StringView].
  Node lowercases incoming keys while native keeps wire casing, so
  event.req.headers.get("X-Test") behaved differently per backend even
  though HTTP field names are case-insensitive.

Additionally the JS adapter did its own inline dispatch instead of
reusing the shared dispatch_http, so the JS serve path was not covered
by the in-process dispatch tests.

This change defines one backend-independent request contract:

- Routing splits the request-target into a pure path and a query
  (query string retained separately on HttpRequest.query; the path is
  used for route matching and static asset resolution).
- A shared request_has_body predicate drives body reading identically
  for both adapters (POST/PUT/PATCH always; or framed via
  content-length/transfer-encoding).
- HttpRequest.headers and HttpResponse.headers are now owned,
  case-insensitive Map[CaseInsensitiveString, StringView]; the implicit
  String -> CaseInsensitiveString conversion keeps existing call sites
  (map literals, .get/.set/[...]) compiling unchanged while making
  lookups case-insensitive.
- The JS adapter now funnels through the shared dispatch_http, so the
  JS serve path gets the same normalization, body reading, and error
  handling as native.
- The mongoose C adapter builds a case-insensitive header map and
  inherits the query-splitting fix via dispatch_http.

A request-conformance suite (request_conformance.mbt) runs on both the
js and native targets and asserts: query strings do not break routing,
path and query are exposed separately, fragments are stripped, PUT and
PATCH bodies reach the handler, and request headers are
case-insensitive.

Verified: moon test passes on both targets, and a live smoke test
drives the native and JS servers identically for query routing, PUT/
PATCH bodies, and mixed-case headers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Applies `moon fmt` reformatting to the changed sources and the
responder tests, and `moon info` to regenerate pkg.generated.mbti for
the updated public API (headers keyed by CaseInsensitiveString; the
new HttpRequest.query field).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@oboard
oboard merged commit 12b7610 into main Aug 21, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant