Skip to content

Fix custom HTTP response status codes arriving as 200 at the client#12

Open
sclaiborne wants to merge 3 commits into
fix/server-receive-robustnessfrom
fix/response-status-code
Open

Fix custom HTTP response status codes arriving as 200 at the client#12
sclaiborne wants to merge 3 commits into
fix/server-receive-robustnessfrom
fix/response-status-code

Conversation

@sclaiborne

Copy link
Copy Markdown
Member

Problem

Reported against @loupeteam/llhttp 1.0.0 (ARsim, AR 6.5.1): the status input of LLHttpResponse appears to be ignored — every response arrives at the client as HTTP/1.1 200 OK, even when the application sets 202/409/400 in the same cycle as send := TRUE.

Root cause

LLHttpUriMatch() returned a match as soon as the request URI reached its query string, without requiring the handler pattern to be consumed too (HttpMisc.c):

if(*b == '?') { // We do not support matching parameters
    return 1; // We have matched everything but the parameters
}

So POST /api/brew?durationS=180 matched a handler listening on /api/brew/abort in addition to the /api/brew one. The server dispatches to every matching handler, so both LLHttpResponse FBs answered the same request, and whichever response was queued first reached the client — typically a sibling route''s 200 that masked the real route''s 202/409. Requests without a query string dispatched correctly, which is what made the symptom look like "the status input is ignored": the response body came from the right family of routes while the status was always 200.

The status input itself was never broken: driving the full LLHttpServer + LLHttpResponse path in the off-PLC harness shows 200/202/204/400/404/409/500 all reaching the wire correctly, on this branch and on the v1.0.0 sources. (The published 1.0.0 binary was also ruled out: it was built by CI three minutes after the v1.0.0 tag commit, and the packaged .fun/.typ are byte-identical to the tag.)

Changes

  • LLHttpUriMatch: a query string in the request URI only counts as a match if the handler pattern is exhausted at that point. Wildcard patterns (*, **) and exact matches with queries behave as before.
  • LLHttpBuildResponse: a response whose status was left at 0 now goes out as 200 OK instead of the malformed HTTP/1.1 0 status line (which strict clients such as .NET HttpClient reject outright).
  • LLHttpBuildResponse (in passing): empty-payload responses now carry an explicit content-length: 0 (except 1xx/204/304, which must not have one), so keep-alive clients don''t wait for a body that never comes.

Tests

  • End-to-end FB test: LLHttpServer + LLHttpResponse driven through the stubbed TCP layer; asserts the wire status line for 200, 202, 204, 400, 404, 409, 500, and the status-0 default.
  • Regression test for the dispatch bug: two routes (POST /api/brew, POST /api/brew/abort), a query-string request, and an immediate 200 from the sibling route — asserts the sibling never sees the request and the wire carries the 202.
  • LLHttpUriMatch unit cases for query-string URIs (including the /api/brew?x vs /api/brew/abort false match).
  • LLHttpBuildResponse unit cases for non-200 status, status-0 default, content-length: 0, and 204 omitting content-length.

All 11 test cases pass off-PLC (32-bit MinGW via the loupeRT harness); the 4 new/extended ones fail without the fix.

Notes

🤖 Generated with Claude Code

sclaiborne and others added 3 commits June 11, 2026 19:09
Covers the LLHttpResponse status input end-to-end through the server
send path, plus LLHttpBuildResponse unit coverage for non-200 codes,
the status 0 default, and content-length on empty payloads.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
LLHttpUriMatch returned a match as soon as the request URI reached its
query string, without requiring the handler pattern to be consumed too.
A request like 'POST /api/brew?durationS=180' therefore dispatched to a
handler listening on '/api/brew/abort' as well as the '/api/brew' one.
Every matching LLHttpResponse FB then answered the same request, and
whichever response was queued first reached the client - typically a
sibling route's 200 that masked the real route's custom status (202,
409, ...). Requests without a query string were unaffected, which made
the bug look like the status input was ignored.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A response queued with status 0 (the FB input's initial value) went out
as the malformed status line 'HTTP/1.1 0 ', which strict clients such
as .NET HttpClient reject outright. Default to 200 OK so applications
that never set the status input keep working.

Responses with an empty payload now carry an explicit content-length: 0
(except 1xx/204/304, which must not have one) so keep-alive clients do
not wait for a body that never comes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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