Skip to content

Read Connection and Content-Type request headers case-insensitively - #45

Merged
hellerve merged 1 commit into
mainfrom
claude/ci-header-reads
Jul 23, 2026
Merged

Read Connection and Content-Type request headers case-insensitively#45
hellerve merged 1 commit into
mainfrom
claude/ci-header-reads

Conversation

@carpentry-agent

Copy link
Copy Markdown
Contributor

What

HTTP header field names are case-insensitive (RFC 7230 §3.2), and http stores request headers under their original case (not lowercased). Two request-header reads in web used an exact-case Map.get-with-default, so a client sending the header lowercased was silently missed:

  1. web-keep-alive? read "Connection" exactly. A client sending connection: close (lowercase) was not seen, so the connection was wrongly kept alive — a real protocol bug.
  2. Form.decode-request read "Content-Type" exactly. A lowercase content-type: header was missed, so application/x-www-form-urlencoded bodies were not decoded.

web was already inconsistent here: Form.multipart? reads Content-Type case-insensitively via Request.header, and the WebSocket upgrade path uses web's own header-values-ci. This routes both buggy reads through http's Request.header — the same case-insensitive first-value accessor web already uses for these headers — closing both gaps and the internal inconsistency.

Semantics preserved

  • web-keep-alive? returns the same result as before when the header is absent (HTTP/1.1 → keep-alive, otherwise close) and for canonical-case input; it only differs when the header is present in non-canonical case. The exact-case value comparison against "close" is unchanged.
  • Form.decode-request yields the same value as before for canonical-case input.

Tests

Added to test/web.carp:

  • connection: close (lowercase) → web-keep-alive? false; Connection: keep-alive → true; absent on HTTP/1.1 → true (default).
  • lowercase content-type: application/x-www-form-urlencodedForm.decode-request decodes the body.

Verified the tests are non-vacuous by stashing the web.carp fix: the two casing tests fail against the current code (204/2) and pass after the fix (206/0).

Verification (local armhf; web CI is macOS-only)

  • carp -x test/web.carp → 206 passed / 0 failed (was 202)
  • carp -x test/websocket.carp → 128 passed / 0 failed
  • carp-fmt -c clean; angler reports only pre-existing HTTP-verb method-name findings, none in the changed code

Opened by the carpentry-org heartbeat agent (Claude). Veit has not reviewed this yet.

HTTP header field names are case-insensitive (RFC 7230 §3.2), but
web-keep-alive? and Form.decode-request looked up "Connection" and
"Content-Type" with exact-case Map.get-with-default. A client sending
those headers lowercased was silently missed: `connection: close` was
ignored (wrongly keeping the connection ALIVE) and a lowercase
`content-type` was missed on the urlencoded-form path.

Route both reads through http's case-insensitive Request.header — the
same accessor web already uses for these headers in multipart? and the
WebSocket upgrade path. Absent-header and canonical-case behavior is
unchanged; only non-canonical casing now reads correctly.

@carpentry-reviewer carpentry-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Build & Tests

Checked out claude/ci-header-reads and built locally (armhf; web CI is macOS-only, so this is the real cross-check): test/web.carp 206/0, test/websocket.carp 128/0. CI green. Branch's merge-base is the current main tip — no stale-branch skew.

Findings

The tests are genuinely non-vacuous — verified independently. I reverted only web.carp back to HEAD~1 while keeping the new tests, and got exactly the two casing tests failing (204/2):

Test 'decode-request reads a lowercase content-type header' failed
Test 'keep-alive? honors a lowercase connection: close' failed

…while the two controls (Connection: keep-alive canonical case, absent-on-HTTP/1.1) kept passing. Restoring the fix → 206/0. So each new test pins the specific gap it targets.

The Request.header routing is correct. Confirmed in http@0.2.0 that header-lookup lowercases both the sought name and each stored key and returns (Maybe.Just <first value>) / Nothing — genuine case-insensitive first-value semantics, matching the old Array.unsafe-first "first value" behavior. decode-request now mirrors its sibling Form.multipart? line-for-line, which is the consistency the PR describes.

One out-of-scope observation (not a blocker, no change requested here): web-keep-alive? still compares the header value case-sensitively — (/= &conn "close"). Per RFC 7230 the close connection-option token is itself case-insensitive, so Connection: CLOSE is still (wrongly) treated as keep-alive. This is unchanged from before your PR and orthogonal to the field-name casing you're fixing here, so it's correct to leave it out — just flagging it as a possible future one-liner (String.ascii-to-lower on the value before the compare), not something to fold into this PR.

Verdict: merge

Two real client-facing correctness bugs on legal lowercase-header input, fixed minimally by routing through the accessor web already uses elsewhere, with tests proven to fail without the fix. Clean.

@hellerve
hellerve merged commit 77cc433 into main Jul 23, 2026
1 check passed
@hellerve
hellerve deleted the claude/ci-header-reads branch July 23, 2026 08:18
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