feat: unify the HTTP request contract across backends - #28
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The three HTTP backends (native, JS/Node, mongoose C) produced materially different
HttpRequestvalues for the same request, so application behavior depended on which backend a deployment used:?querybefore dispatch, so the handler could not recover it; JS routed against the rawreq.url(path + query), soGET /hello?x=1returned 404 against a registeredGET /helloroute.POST, so PUT/PATCH reached handlers with an emptyraw_body.Map[StringView, StringView]. Node lowercases incoming keys while native preserves wire casing, soevent.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:
dispatch_httpnow splits the request-target into a pure path (used for route matching and static-asset resolution) and a separately-retainedHttpRequest.query. A?querycan no longer 404 a registered route; the handler can recover it.request_has_bodypredicate — drives body reading identically for the native and JS adapters: always for POST/PUT/PATCH, or when a body is framed viacontent-length/transfer-encoding.HttpRequest.headersandHttpResponse.headersare now to-owned, case-insensitiveMap[@http.CaseInsensitiveString, StringView]. BecauseCaseInsensitiveStringis implicitly convertible fromString/string literals, existing call sites (map literals,.get/.set/[...]) compile unchanged while lookups become case-insensitive. The mongoose adapter'sparse_headersreturns the same type.dispatch_http— reusing shared routing, body reading, and error handling, so the JS serve path conforms to the same contract.Conformance suite
request_conformance.mbtadds 6 tests that run on both the+jsand+nativetargets (the workspace compiles/runsdispatch_httptests per-target). They assert:Verification
moon checkclean on both+jsand+nativetargets.moon testpasses on both targets (67 native, 73 js).GETroute and aPUTecho handler, run twice:GET /hello/moonbit?x=1&y=2path=/hello/moonbit query=x=1&y=2path=/hello/moonbit query=x=1&y=2PUT /echo/42body +x-test: abc(thenX-Test: MIXED,X-TEST: CAPS)body=payload-body header=abcbody=payload-body header=abc(thenMIXED,CAPS)Identical on both backends.
🤖 Generated with Claude Code