Repository navigation
π Bug Report β Runtime APIs: empty If-Match disappears at HTTP ingressΒ #7867
Description
Description
An explicitly empty If-Match: field arrives over the real HTTP socket, but the Worker sees it as absent: request.headers.get("If-Match") returns null instead of "". This occurs through the Miniflare frontend and through a direct workerd socket that bypasses that frontend.
The original application symptom was a conditional GET expecting 412 returning 200. The field is already missing before application handling, so this report is about transport preservation, not a request for an application workaround.
Observed environment
- Linux x86_64; workerd npm package
1.20261021.1, executable reporting2026-10-21. - Miniflare
5.20261019.1-betafrom the existing Wrangler4.151.3installation; Node26.12.0. - Worker compatibility date
2026-03-23. - The actual runtime binary matches the official npm binary for workerd source
1038810cb01af7860b0d576ee893ceb656bd5e98; its pinned KJ/Cap'n Proto dependency isa49c8fb48462987aafd612220de2d7a6c3f3d211.
These are observations of this local runtime, not a claim about deployed Cloudflare Workers, every platform/version, or HTTP/2 and HTTP/3.
Real-transport reproduction
Use the following receiver Worker and send GET /robots.txt over its actual HTTP socket, distinguishing an absent field, an empty field (If-Match: followed immediately by CRLF), and If-Match: "fixture-tag".
export default {
fetch(request) {
const url = new URL(request.url);
return Response.json({
line: request.method + " " + url.pathname,
value: request.headers.get("If-Match"),
});
},
};This is the tested receiver body with formatting expanded. In the executed matrix, every request came from the same native canonical HTTP API. A transparent TCP pass-through recorded and forwarded the original bytes without inserting/replacing headers. No in-memory Request, dispatchFetch(), substitute client, carrier header or application workaround was used.
| Supplied field | Plain HTTP receiver | Miniflare frontend -> Worker | Direct workerd socket -> Worker |
|---|---|---|---|
| Absent | null |
null |
null |
| Empty | "" |
null |
null |
"fixture-tag" |
exact value | exact value | exact value |
The empty field is present in the TCP observation for both affected routes. The nine-case native transport matrix executed 7 passes / 2 failures; only those two empty Worker cases failed. The diagnostic receiver intentionally returns JSON/200 for all variants: its oracle is the exact field value, not the original application's separate 412 assertion. Ordinary unconditional GET behavior was also checked separately.
Identified upstream cause
- The factory shares the header-table builder with
HttpOverCapnpFactory, whose common-name table includesIf-Match. - The Hyper ingress
HeaderBlockcopies the zero-length value and its length.headers_from_blockinsrc/rust/cxx/kj-hyper/kj-hyper.c++callsHttpHeaders::addPtrPtrwith it. - KJ stores indexed values as
StringPtr, using== nullptras the absent test. KJ's emptyStringPtralso compares equal tonullptr, so insertion loses distinguishable presence; lookup/iteration/clone/serialization then omit the field. - The Worker Headers constructor copies via KJ iteration. Unindexed empty fields do not have the same representation problem.
The proposed upstream correction is capnproto/capnproto#2878 (draft). It stores indexed presence independently using Maybe<StringPtr>, without changing global StringPtr semantics, the client or application policy. Four native KJ regressions fail on the original source and pass with the patch; the complete native HTTP test executable passed 153/153 cases on the pinned source plus the fix. Those counts include the four new cases, and are not a patched workerd result.
The KJ PR targets the newer v2 base 4903846ea6a0f32b2be3deaf8009795829c3f70c, with the exact same three HTTP file changes. Its current-base rerun is pending CI because fresh local capacity planning did not permit another build. No rebuilt-workerd or post-fix application 412 result is claimed.
Requested resolution
Once the KJ fix is accepted, update workerd's selected dependency and add a real HTTP-ingress regression that preserves absent/empty/nonempty header distinctions. We will follow with that dependency/regression PR after acceptance. The original 412 assertion, unconditional GET behavior and deadlines should remain unchanged.
Reference: RFC 9110 If-Match. This report does not assert a security severity, CVE, authentication bypass or production-platform exploit.
AI-assisted investigation and contribution.
Activity