Repository navigation
Releases: faratech/httpjet
Release list
httpjet v0.0.5
httpjet 0.0.5 — whole-codebase audit remediation
This release fixes the 15 findings of a whole-codebase bug sweep (2026-10-12: four parallel reviewers across the transport, page-cache, LSAPI/rewrite, and proxy/ACL/TLS surfaces, every finding hand-verified in source and the worst one reproduced dynamically). Every fix ships with a regression test. Dependencies are refreshed to their latest releases; the toolchain stays at Rust 1.99.
Availability (high)
- Rewrite
[S=n]infinite loop. A rule such asRewriteRule ^x$ - [S=18446744073709551615]wrapped the evaluator's rule index back onto itself in release builds, pinning a worker core forever per matching request. Skip advance now saturates past the end of the ruleset. - Unbounded HTTP/2 upstream handshake in the proxy. The
h2:///h2s://handshake could stall indefinitely at the IO layer, pinning a permit from the process-wide dial budget (64) plus an upstreammaxConnsslot. It now runs under the connect budget, mirroring the health-check probe. - HTTP/2 reset without teardown. After the server sent
RST_STREAMfor a body-pending stream (zeroWINDOW_UPDATEincrement, self-referentialPRIORITY), a laterDATA … END_STREAMstill completed and dispatched the already-reset request. The reset path now tears down the receiving stream and returns its buffered-body accounting.
Proxy correctness
- WebSocket targets enforce
maxConns. Default (non-load-balanced) WebSocket upstreams acquired no concurrency permit at all; the long-lived relay was bounded only by global client limits. A permit is now acquired and held for the relay's lifetime. - Failover survives config reloads. Retained upstreams kept writing dial failures to the previous generation's cooldown counter, so after a reload the new pool never saw the primary as bad and fast-failed instead of failing over. Cooldowns are carried into the retained generation and handles are rebound on selection.
Request handling and PHP trust boundary
- Forwarding-header synonyms. The blocklist for proxy-asserted identity headers was an exact-name list;
X-Client-IP,X-Forwarded-Protocol,Proxy-Client-IP, and similar synonyms from untrusted clients reached PHP as spoofable$_SERVERvalues. Matching is now class-based (the*-client-ip,x-forwarded-*, and*-real-ipfamilies, dash/underscore-folded) and fails closed. - CGI env overrides.
[E=…]could replace server-computedREQUEST_METHOD,REQUEST_URI,PATH_INFO,CONTENT_*,GATEWAY_INTERFACE, andREQUEST_TIME*; all are now on the override deny-list. - WAF fail-open no longer corrupts requests. A sidecar timeout mid-body-collection used to forward an emptied body downstream; the body is now collected and restored before the timed inspection exchange.
HTTP/3, page cache, and LSAPI lifecycle
- H3 request targets. A network-path
:path(//evil.example/x) minted a URI authority diverging from the routed Host; rejected. QPACK field sections with a nonzero Required Insert Count or Delta Base are rejected (the dynamic table is advertised at capacity 0). - Purge completeness.
/__hj_cache_purgehonors everyX-LiteSpeed-Purgeheader line instead of only the first, and the tag-purge version sweep can no longer unlink a live re-stored page's body file.enablePostCachenow warns (config lint + startup log) instead of being silently inert. - LSAPI lifecycle. Restart backoff is cancellable on shutdown, registry drain has a wall-clock bound so a mid-start pool cannot be orphaned, and a jail chroot configured without credentials is a hard spawn error rather than a silent skip.
Dependencies
Cargo.lock refreshed to the latest semver-compatible releases (aws-lc-rs 1.18.2, zstd 0.14.2, serde_json 1.0.154, tokio-util 0.7.20, OpenTelemetry 0.33.1, proptest 1.12, and more); all manifests already required the latest majors. cargo audit is clean and the full test suite (2,028 tests) passes.
Production deployments should follow the usual path: scripts/build-pgo.sh, then scripts/deploy-lscache.sh and the cache gate scripts.
httpjet v0.0.4
httpjet 0.0.4 — security release
This release fixes the findings of a whole-codebase security review (2026-10-06) and moves every dependency and the toolchain to their latest versions. Upgrading is recommended for anyone serving HTTP/3.
HTTP/3 (high)
- Connection-budget exhaustion before address validation. A new QUIC connection used to take a permit from the connection budget shared with TCP before the client's address was validated, so spoofed Initials could starve TCP accepts. The budget is now charged when the handshake completes. Once 64 handshakes are pending on a core, Initials from unvalidated addresses get a stateless Retry. Handshakes are capped at 256 per core and time out after 10 s.
- quinn-proto 0.11.19 (GHSA-53rm-773f-4q8c: one MAX_STREAM_DATA frame could open 2^40 streams). The request-stream accept loop also enforces its own bound, independent of quinn: at most 256 new request streams per connection drive and 512 tracked requests per connection, otherwise the connection closes with
H3_EXCESSIVE_LOAD. The same quinn release fixes a client-side Retry issue (GHSA-wppq-2f6r-wfvm) that does not affect httpjet's server role. Thanks to the quinn maintainers for the fixes.
Request handling
- Rewrite targets are decoded once. An internal rewrite target is percent-decoded and normalized once, matching Apache's internal-redirect semantics. Previously, destination-scoped policy (
Headerdirectives, body limits, static contexts) judged the still-encoded path while the static handler decoded and served it. - Client headers reaching PHP/CGI follow one rule. This applies to the LSAPI env table, the LSAPI header block and FastCGI params. Underscore-named headers (which lsphp mapped to
HTTP_*through the header block),Proxy, and forwarding-authority headers (CF-Connecting-IP,X-Forwarded-*,X-Real-IP,True-Client-IP,Forwarded) from untrusted peers no longer reach the application. - HTTP/1.1 request targets and Host. RFC 9112 target forms and a valid
Hostare required. An absolute-form authority replacesHost.CONNECTand heads with bare-LF line endings are refused before dispatch. - Basic auth.
AuthUserFilemust open inside the docroot, checked on the opened descriptor. Use--auth-user-file-rootsfor other locations.- Unknown users pay the same bcrypt cost as known ones.
- bcrypt cost is capped at 14.
- Plaintext htpasswd entries no longer verify, as in Apache on Unix.
- An unusable user file returns 500.
Control surfaces
/__hj_cache_purgeaccepts onlyPOST(405 +Allowotherwise)./__hj_cache_readyaccepts onlyGET/HEAD.checkwarns that unix-socket listeners expose both.- The page-cache integrity key is opened through a validated path.
httpjet_pagecache_integrity_activereports its state, and--page-cache-integrity-requiredrefuses to run the file tier without it. - SIGHUP reloads time out instead of waiting indefinitely. Config files are size-capped. Changes to
proxyProtocol,proxyProtocolPeersor a unix-socket path are classified as restart-required. - New
--profile-token-file. The metrics listener is capped at 64 concurrent connections. The lsphp control socket is created through a rename, so it never exists with looser permissions.
Dependencies and tooling
- Dependencies are refreshed to their latest releases. The vendored forks moved to socket2 0.6, mio 1 and thiserror 2. The unmaintained rustls-pemfile and fxhash were replaced.
cargo audit --deny warningsis clean. - Rust 1.99.0 is now the minimum and CI toolchain.
- New daily
Dependency advisoriesworkflow. It also watches transport-critical crates for releases that advisory databases have not caught up with yet. - New clippy panic-site ratchet for the request-serving crates, since release builds use
panic = "abort". - New fuzz targets:
qpack_decode,proxy_protocol_parse,lscache_control.
Upgrade notes
- HTTP/1.1 clients must send a valid
Host. Absolute-form requests have their authority used asHost. - Operators keeping htpasswd files outside the docroot need
--auth-user-file-roots. Plaintext htpasswd entries must be re-hashed with bcrypt (htpasswd -B). - Local purge scripts must use
POST.
httpjet v0.0.3
httpjet v0.0.3 — security and hot-path fixes
This release fixes several ways static or precompressed file serving could expose script source, blocks proxied responses from setting internal cache controls, and restores local purge handling when a rewrite proxy route is disabled. It also tightens the page-cache file tier and reduces work in HTTP/3 deadlines, cache hits and startup, PHP suffix routing, LSAPI dispatch, and client throttling.
The page-cache file tier now opens files beneath a pinned root with openat2 where available and uses a secure descriptor walk elsewhere. The new HJPC v3 format discards older v2 cache entries once at startup; expect a cold cache immediately after upgrading if the persistent file tier is enabled.
HTTP/3 load clients are included in the public source so the benchmark suite can exercise QUIC even without an HTTP/3-capable h2load. The standalone fuzz lockfile is updated for 0.0.3.
The deployed x86_64 PGO build passed config, cache-isolation, purge, HTTP/2, HTTP/3, live member, and restart-persistence checks. Paired old/new loopback benchmarks completed with identical bytes and zero request failures. Throughput did not show a repeatable slowdown, although host I/O contention made the measurements too variable to resolve small differences reliably. Public CI, protocol interoperability, and bounded fuzzing were run against this source snapshot.
httpjet v0.0.2
httpjet v0.0.2 — September 2026: correctness fixes, page-cache efficiency, lower per-connection cost
This release carries everything since v0.0.1 (September 17): a review round of correctness and
hardening fixes, page-cache CPU work, progressive streaming for cacheable misses, lower
per-connection memory, and a dependency refresh. New features are opt-in; defaults that changed
are listed under Behavior changes below.
Measured figures come from one production configuration (a CDN-fronted forum plus two
template-driven sites) or from A/B runs against the previous build, and will vary with workload.
Fixes
- Rewrite
[P]targets: the loopback screen now pins the dial. The screen resolved the target
name once, then the proxy dialed by name and resolved again, so a DNS answer that changed between
the two (rebinding) could still reach loopback. The dial now uses the exact address that was
screened. IP-literal targets skip the resolver entirely. - CGI environment.
SSL_CLIENT_*variables can no longer be overridden bySetEnvIf/[E=]; they are denied
like the other server-computed variables.- A user verified by a scoped Basic-auth realm reaches the app as
REMOTE_USER/AUTH_TYPEagain. - The page cache's stale-while-revalidate refresh marker (
HJ_CACHE_REFRESH) reaches the app
again, so an application-level cache can no longer answer a background refresh with its own
aging copy. - v0.0.1's environment deny-list had dropped both of these.
.htaccess.- An unreadable file is still deny-all (fail closed), but the deny now heals as soon as the file
is readable again. It used to persist until the file's mtime changed, andchmoddoesn't
change mtime. - The per-directory walk stops at the first directory that does not exist, so front-controller
URLs no longer fill the cache with negative entries. It used to saturate at 16,384 entries and
stop memoizing new directories.
- An unreadable file is still deny-all (fail closed), but the deny now heals as soon as the file
- HTTP/2.
- WINDOW_UPDATE credit that arrived for an already-finished stream was parked indefinitely, and
on long-lived connections could exhaust the table that holds pre-response credit. - A stream reset by the body deadline now accepts late trailers without a connection-wide GOAWAY.
- Receive-side WINDOW_UPDATEs are batched at half the window instead of sent twice per DATA frame.
- WINDOW_UPDATE credit that arrived for an already-finished stream was parked indefinitely, and
- Stale-if-error during a PHP outage. When the PHP pool stopped answering, a failed request first
rendered a PHP-handledErrorDocumentthrough the same failed pool, and only then served the
retained stale copy. That doubled the wait: in our deployment 120 s, past a typical CDN origin
timeout. A generated 502/503/504 from a PHP dispatch now skips PHP-handled error documents, so the
stale copy (or the built-in error page) arrives after one backend timeout. - Page-cache admission counted twice. A cookieless request probed the cache on the on-core fast
path and again in dispatch, and both recorded an admission sighting. As a result
--page-cache-admit-threshold 2behaved like 1, and miss counts were doubled. One request is now
one sighting.
Page cache: less CPU for the same content
- Store-time dictionary compression defaults to zstd level 12 instead of 15. In our A/B that
was 2.7-2.9x faster for 1-2% larger stored bodies.- New
--page-cache-dict-level vhost=level,...sets a level per vhost. - Encoders are pooled per dictionary.
- In our deployment, dictionary encoding had been the largest share of httpjet's own CPU.
- New
- Fewer encodes.
- A capsule render stored under two keys is encoded once.
- Incompressible content types and tiny bodies are never encoded.
- The skip reasons are exported as
httpjet_pagecache_dict_recompress_skipped_total{reason}.
- Cheaper bookkeeping.
- Entry metadata is compressed at zstd level 6 instead of 19.
- Weak ETags are hashed a word at a time.
- A decoded body is built once per hit.
- Store-time finalize paths no longer pin decoded metadata in RAM.
- Body collectors are presized from Content-Length.
- Streaming zstd compression contexts are reused across responses.
- Miss responses carry the stored entry's weak ETag, so a CDN can revalidate them with
If-None-Match. - New observability: precompressed-variant fill/serve counters per codec, and sampled phase
histograms for store and body collection.
New opt-in features
--page-cache-stream-fill-vhosts <vhosts>: progressive miss streaming.- For templates that print without a Content-Length, a cacheable miss is forwarded to the client
as the backend writes it, then stored once the backend finishes cleanly. It used to be
buffered whole before the first byte went out. - The transport sends the head with the first byte, and the streamed compressor flushes its
first block when the backend stalls. - A backend error, a client disconnect, or a body over the object cap stores nothing.
- Single-flight followers still wake to a stored entry.
- In our deployment it moved a progressive site's miss time-to-first-byte from ~9 ms to ~6 ms.
- For templates that print without a Content-Length, a cacheable miss is forwarded to the client
--page-cache-opt-in-status 303,404,410+--page-cache-303-prefixes <prefixes>. Cache these
statuses only on an explicitX-LiteSpeed-Cache-Control: public, with the TTL capped at 300 s and
no stale windows. A 303 is cached only under a listed path prefix.--trusted-proxy-idle-secs N+--trusted-proxy-idle-max-conns N. A longer idle window for
connections whose peer presented a verified client certificate, such as a CDN's authenticated
origin pulls, which never resume TLS. At 600 s this cut full TLS handshakes per request by ~55%
in our deployment. It falls back to the normal floor above the connection cap.--tls-session-tickets N(default 2, unchanged behavior).0stops issuing TLS 1.3 tickets
and shrinks the session cache to TLS 1.2 session IDs, for deployments whose peers never resume.
It measured ~5% less server CPU per handshake.HTTPJET_LOG_DIR=<dir>redirects every log file an instance opens (unified and per-vhost). It
is meant for test instances that load a production config.
Lower per-connection cost
- Idle TLS connections hold no write buffer. A flushed 64 KiB ciphertext buffer returns to a
per-thread pool (cap 32) and the next write takes a warm one, so a response costs no allocation or
page fault. In a 2,000-connection A/B this used 19-30% less memory per idle TLS connection, with
throughput unchanged within noise. - HTTP/2 per-connection stream maps start empty instead of being pre-sized at accept.
- TLS parameters for a repeat client certificate are memoized instead of re-parsed on every
handshake.
Behavior changes to check when upgrading
--page-cache-admit-thresholdnow means what it says (see the admission fix above). If you
set 2 while it behaved like 1, set 1 to keep the previous store rate.- The connection-close mimalloc collect uses
--mimalloc-trim-threshold-mib(default 768)
instead of a hard 384 MiB cap. A large page cache keeps a healthy process above 384 MiB, which
made every thread force-collect every 30 s. Size the flag above your healthy peak RSS; in our
deployment minor page faults fell from ~6,600/s to ~400/s. - Default store-time dictionary level is 12 (was 15). Bodies stored at 15 still decode.
- HTTP/2 receive-side WINDOW_UPDATE batching changes wire behavior (h2spec 2.6.0: 146/146 over TLS).
Public repository and CI
- CI and the interoperability workflow now install Rust 1.98.1. They had still pinned 1.97.0, below
the workspace floor, so scheduled runs stopped at the toolchain check. - The helper scripts the workflows call are now published: ACME and OCSP acceptance, h2spec,
bounded fuzzing, the OCI smoke test, and two HTTP/3 test clients. fuzz/Cargo.lockis current, and the OCI example builds onrust:1.98-bookworm.- The OCI smoke test works on non-root runners.
Dependencies
- opentelemetry 0.33 (optional
otelfeature), plus cc, hyper-rustls, rand, and unicode-ident. - Built and tested with Rust 1.98.1.
httpjet v0.0.1
httpjet v0.0.1 — September 2026: security hardening round + new subsystems
First tagged release, adopting semantic versioning.
This sync carries everything since the previous public snapshot (September 6): a set of new
opt-in subsystems, a transactional zero-downtime reload architecture, and a whole-codebase
security review whose twelve fixes are the centerpiece of this release. As always, every
subsystem below is opt-in — the default configuration serves exactly as before.
Security hardening round (whole-codebase review)
Five parallel review slices covered the request pipeline, transports, TLS and unsafe code, trust
and access control, backend gateways, cache and static serving, control surfaces, process
supervision, ACME, logging, and ops packaging. Twelve fixes shipped, all fail-closed:
Transport hardening
- HTTP/2: frame and request-body deadlines on the production loop. The production (monoio)
h2 loop never enforcedframe_read_timeout/request_body_timeout— those deadlines existed
only in a code path that doesn't run — so a peer trickling one byte per idle window could pin a
connection, its header block, and its body buffers indefinitely. The production loop now
mirrors the tokio-loop bookkeeping: partial frames must complete within the frame deadline
(GOAWAY PROTOCOL_ERROR otherwise), and requests with decoded headers but no END_STREAM are
reset with body accounting released after the body deadline. - HTTP/1.1: stalled
sendfileis bounded. A zero-window reader used to cause ~10k
sendfile(2)syscalls/s per connection on a pinned worker core, forever. Retries back off
exponentially (100 µs → 12.8 ms, reset on progress) and a transfer with zero progress for 60 s
closes the connection. - HTTP/1.1: duplicate identical
Content-Lengthlines collapse to one on the forwarded
header map, so a lenient upstream cannot re-parse two lines differently. - CPU pinning respects
CPU_SETSIZE—available_parallelism()can exceed the fixed
1024-bit affinity mask on pathological hosts.
Trust boundary
- PROXY protocol: claimed loopback/unspecified sources are refused at the parser. The
pp-claimed source was applied before the mandatory client-certificate gate, so on a
pp-enabled public listener a direct client could claim127.0.0.1and be treated as a trusted
internal peer: skipping the client certificate, reaching the loopback-gated cache purge/ready
endpoints, and bypassing the plaintext mTLS-gate redirect. Such headers are malformed in both
v1 and v2 (v4-mapped forms included) and the connection is closed. Real load balancers never
relay loopback client addresses. - Rewrite
[P]targets are screened away from the local trust plane.[P]URLs are
request-influenceable (.htaccessauthors;$Nbackrefs in the authority), and a same-host
dial presents a loopback raw peer — exactly what the purge/ready endpoints, the metrics
listener, and the admin listeners gate on. Rewrite-driven targets refuseunix:/uds:
transports outright and resolve the authority (bounded 3 s) to reject loopback, unspecified,
and link-local addresses — including/etc/hostspins, which bypass literal screens.
Config-declared proxy targets remain operator-trusted and unscreened. - CGI environment: directory-authored overrides of server-computed variables are denied
(shared by LSAPI and FastCGI). Rewrite[E=...]/SetEnvIf entries could previously re-point
SCRIPT_FILENAMEat an arbitrary readable file or forgeREMOTE_ADDR/HTTPS/SERVER_*.
SCRIPT_NAME,QUERY_STRING,REDIRECT_*(front-controller and ErrorDocument contracts) and
HTTP_*mirrors remain overridable.
Fail-closed defaults
.htaccess: unreadable or non-UTF-8 access files no longer degrade to "no rules." A
present-but-unreadable file, or a single non-UTF-8 byte, memoized the directory as unprotected
until the mtime changed. Non-UTF-8 files decode lossily (rules survive); any other read error
yields the deny-all sentinel, self-healing on mtime change — matching the existing parse-error
behavior.- lsphp: never-root is enforced on every credential path, and a setuid/setgid lsphp binary
refuses the spawn outright (after the supervisor's credential drop, executing such a binary
yields workers whose effective uid is root). - Isolation modes: out-of-range
setUIDMode/chrootModevalues fail the vhost load instead
of silently disabling isolation.
Cache integrity
- The page cache varies stored entries on
Originwhen the.htaccesschain reads it. An
origin-reflecting chain (SetEnvIf /RewriteCond %{HTTP:Origin}, typically for a reflected
Access-Control-Allow-Origin) produces origin-conditional responses, but the cache stored
whatever headers the first storer's render produced — every later visitor received that
request's CORS verdict. Origin now folds into the cache-key vary discriminant (bounded hash)
across lookup, store, stale-if-error, and capsule keys. Absent/emptyOriginkeeps the
historic base key, so entries persisted before this change stay hit-able; origin-bearing
requests (browser fetch/XHR) re-render once per URL per origin after upgrading.
Admin write API and packaging
useIpInProxyHeaderand the whole<security>block (trusted-CIDR access rules,
followSymlink, geo/ASN lists) are restart-required rather than silently publishable as
live "application" updates; unix-socket ext-processor addresses are authorized against
--admin-resource-root.- The OCI seccomp profile gates
ptrace/process_vm_*onCAP_SYS_PTRACE; the page-cache
integrity key is createdO_CREAT|O_EXCLat mode 0600 (no umask window, no symlink redirect).
New subsystems since the previous sync (all opt-in)
- FastCGI gateway — bounded wire protocol, connection pool, CGI response parsing, streaming
handler, and suffix routing. - WAF sidecar — request inspection before cache and backends with bounded aggregate memory;
when configured, every request funnels through inspection (fast paths decline). - ACME client — durable HTTP-01 issuance/renewal with TLS activation, plus scoped DNS-01
wildcard issuance with durable cleanup. - OCSP stapling — opt-in validated staple lifecycle with fail-closed "required" mode.
- Admin APIs — read-only loopback introspection (
GET /v1/status) and an opt-in write
transaction API: token-file auth, bounded control requests, JSON config submissions validated
against operator-declared resource roots, and revision-checked publications that leave the
current configuration serving on any failure. - Transactional zero-downtime reload — configuration changes are classified; TLS/cert and
trust-policy changes flow through prepared same-socket TCP successors with kernel-verified
socket-identity handoff, raced-accept transfer, activation gating, and generation-pinned
connection views. Failed candidates leave the currently serving generation untouched. - Reverse-proxy load-balancing groups — weighted selection, strict group configuration
(canonical unix syntax, validated ports), bounded active health probes, and generation-scoped
pool lifecycle. - OpenTelemetry — bounded OTLP trace/metric export with an explicit trust policy for inbound
trace parents; spans cover H1/H2/H3, WebSocket relays, LSAPI dispatch, and cache/rewrite
stages; export never blocks request handling. - kTLS — auto mode gated on real NIC TLS-hardware offload; H1 file bodies send via
sendfile
on plaintext and kTLS connections. - Logging — structured JSON error events with credential redaction, an optional syslog tap,
and a shared parser corpus replay. - Bounded request decompression for
Content-Encodingrequest bodies, and a hardened OCI
packaging example.
Dependency refresh
All dependencies bumped to their latest releases, including major versions: rustls
0.23.45 (closes RUSTSEC-2026-0285), tokio 1.53.1, quinn 0.11.12, sha2 0.11, hmac 0.13,
zstd 0.14, brotli 9, base64 0.23, bcrypt 0.19.3. The vendored monoio fork was ported to
io-uring 0.7.15 (upstream monoio 0.2.4 remains the latest release): the fork's multishot
accept machinery re-applies unchanged on the 0.7 Builder<S, C> defaults, and the dependency
graph dedups to a single io-uring version. The minimum supported Rust is now 1.98.
Upgrade notes
- PROXY-protocol listeners: connections whose PROXY header claims a loopback or unspecified
source are now closed by design. If a fronting load balancer sends such headers, fix the LB. - Rewrite
[P]rules targeting loopback, link-local, or unix sockets now return 403 — declare
same-host backends as config proxy contexts instead; operator configuration is trusted,
rewrite-driven targets are not. [E=SCRIPT_FILENAME]-style env overrides of server-computed CGI variables are ignored;
front-controller rewrites usingSCRIPT_NAME/QUERY_STRINGare unaffected.setUIDMode/chrootModevalues outside 0–2 abort the affected vhost config load instead of
silently disabling isolation.- Everything else is behavior-identical for conformant clients; new subsystems activate only via
their flags.
Validation
- Full workspace test suite (16 crates, ~1,500 tests) green; new regression tests cover every
security fix, including io_uring-driver tests that drive the real h2 deadline paths. - fast-memo parity gate: 192-case byte-identity matrix on a real 800-line
.htaccesschain —
zero differences. - Page-cache cross-URL, cross-scheme, and cross-host gates green; file-tier persistence gate
restored 148k entries across a restart; private-tier session-isolation gate green. - A live probe on an origin-reflecting chain confirms per-origin CORS caching: a no-Origin
request no longer replays the first storer'sAccess-Control-Allow-Origin.
Co-Authored-By: Claude Code noreply@anthropic.com