Repository navigation
Apply HTTP request queue limits to GET, HEAD and OPTIONS - #3234
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces separate queue depths and limits for HTTP read and write requests, allowing GET, HEAD, and OPTIONS requests to be throttled independently of data-modifying requests. It updates the configuration schema, adds validation, and implements the separate queues across Node, uWS, and Bun environments, supported by comprehensive integration and unit tests. The review feedback suggests returning a safe, empty response descriptor instead of undefined when a read request is aborted in the uWS handler to prevent downstream TypeErrors. Additionally, it recommends explicitly joining array-based CORS headers with a comma and space to ensure proper formatting, and simplifying string coercion in the configuration validator by removing unnecessary typeof checks.
Add an independent, configurable 60-second read queue across HTTP transports while preserving existing write dispatch. Register and validate the setting, consolidate listener routing, and cover queue isolation and HTTP shedding/recovery. Co-Authored-By: OpenAI Codex (GPT-6.1) <noreply@openai.com>
Add an explicit read-admission opt-out, skip disconnected queued reads, and retain CORS and configured headers on read rejections. Validate the native Bun fetch path separately from Bun's node:http compatibility listener. Co-Authored-By: OpenAI Codex (GPT-6.1) <noreply@openai.com>
Reject quoted read-limit scalars without silent Joi coercion, validate the configured CORS allow-headers value before the rejection path can use it, and document preflight and listener configuration boundaries. Co-Authored-By: OpenAI Codex (GPT-6.1) <noreply@openai.com>
Validate the values as Headers.set serializes them, retaining existing string, list and scalar inputs while rejecting control characters. Exercise header lists over each HTTP runtime. Co-Authored-By: OpenAI Codex (GPT-6.1) <noreply@openai.com>
GET, HEAD and OPTIONS no longer pass through the throttle() FIFO. An admitted read dispatches synchronously; a read arriving after the current event-loop iteration has run longer than http.readRequestQueueLimit (default 5000ms) gets a 503 without running its handler. One lazily armed setImmediate probe per thread measures the iteration and stops after a read-free iteration. Writes keep the existing queue. The disconnected-read skips added for queued reads are removed, since reads no longer wait. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…limit to every listener Rejected-read CORS headers are resolved per listener and fall back to the default allow-headers when the configured value is not a valid header, since the 503 callbacks run outside the handler's error handling. Every listener, operations API included, reads http.readRequestQueueLimit, so false disables read admission everywhere. A listener with neither queue returns its handler unwrapped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… boot flattenConfig() merges customFunctions.network over http after validation, so the listener-time fallback is replaced by validating that override with the same rule as http.corsAccessControlAllowHeaders. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… access list on rejected reads Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
flattenConfig() lowercases keys and merges customFunctions.network over http after configValidator() runs, so a lowercase or legacy spelling escaped the http rules. validateConfig() now checks http_readrequestqueuelimit and http_corsaccesscontrolallowheaders on the flattened config, replacing the customFunctions.network schema entries. Dispatch-Task: pr-maint-0cc9e6def5ce915ffc5fc4ff87e04940 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-Authored-By: Gemini <noreply@google.com>
…view findings Dispatch-Task: pr-maint-0cc9e6def5ce915ffc5fc4ff87e04940 Co-Authored-By: Gemini <noreply@google.com>
… limit Dispatch-Task: pr-maint-0cc9e6def5ce915ffc5fc4ff87e04940 Co-Authored-By: Gemini <noreply@google.com>
d2c124a to
a1a2f37
Compare
⊙ Problem
Read-heavy applications have no HTTP admission control: GET, HEAD and OPTIONS never get a 503, even when the worker is overloaded. Closes #2755.
🤔 Reject reads, never queue them
Reads get a 503 under overload and are never queued. Queuing them cost 7–16% of cached-read throughput.
💡 Solution
An admitted read runs right away, in the call that received it. A read that arrives after the current event-loop iteration has already run longer than
http.readRequestQueueLimit(default 5000 ms) gets a 503 without running its handler. One per-thread probe, armed only while reads arrive, measures the iteration.falseturns read admission off. Writes keep the existing queue.⚖️ Alternatives
🤔 Default on at 5000 ms
Once an iteration runs past 5 s, reads get 503s, health checks and preflights included. A 128-client loop of 20 ms renders (~2.5 s iterations) stays under it. The alternative is opt-in.
0rejects every read while reads keep arriving (only the first read after an idle iteration is admitted).🤔 Measure only the current iteration
A one-off stall does not shed the next iteration's reads. The first read after idle cannot see an earlier stall. Reads awaiting slow I/O are not bounded.
Framing-Verdict: chosen-approach-sound
🔧 Changes
Read admission, rejection headers and configuration
Product and architecture tour
Admission
http.readRequestQueueLimit, 5000 msfalse= offhttp.requestQueueLimit, 20000 msBun.serverejectWhenLoopDelayedcompares the current iteration's age with the limit and either calls the handler or the 503 callback. The per-thread probe re-arms from the check phase while reads arrive and stops after one read-free iteration, so an idle worker has no timer. Look hardest at the rejection branch and the probe lifecycle.In
server/http.ts,throttleRequestsuses the reject-only guard for reads and returns the bare handler when neither side is admitted. Every listener reads the one registeredhttpkey: the operations API had no registered copy, sofalsecould not reach it. The same key feeds uWS, the per-thread UDS mirror and native Bun.makeUwsHandlertakes the read limit as a new optional argument.utility/hdbTerms.tsregisters the key.Rejected reads
readRejectionHeadersresolves CORS config once per listener and adds configured response headers,Vary: Originand, for an allowed origin, the CORS headers, without running authentication. It is used by Node, uWS and Bun. The access list is checked withcorsAccessListAllowsso wildcard and legacy string entries matchsecurity/auth.ts. A rejected OPTIONS preflight still fails in browsers, since preflight needs a 2xx.Configuration
The 503 callbacks run outside the request handler's error handling, so their inputs are validated at boot. The two constraints apply to the
httpkeys.effectiveHttpConfigValidatorapplies them again, throughvalidateConfig, to the flattened config listeners read. Flattening lowercases keys and mergescustomFunctions.networkoverhttpafter validation, so a lowercase or legacy spelling would otherwise get past the rules. The schema documents the semantics, and the allow-headers entry documents string and list values. The design note and its index entry record the probe invariant.Cost
Bun.serveand sustained-shed costs are unmeasured.test:unit:mainandtest:integration:allwere not run in full; CI runs them. Typecheck,lint:requiredand Prettier are clean.✅ Verification
PR QA: Do reads shed on async-continuation load and recover, without ever being delayed? — queued @ d2c124a, qa-pr-harper-3234-d2c124a
customFunctions.networkspellings.Bun.servelisteners), with a 1 ms limit, a 10 ms sync resource loaded by its fixture config.🤖 Generated by OpenAI Codex and Claude Opus 5.5; posted via @kriszyp.
Release target: main — new configuration and default read-admission behavior
Related PRs: #3110 overlaps (HTTP dispatch and analytics), #3527 independent (separate config-root.schema.json entries), #3656 independent (separate config-root.schema.json entries), #2752 independent (separate DESIGN.md index entries), #372 independent (status configuration)
Complexity: medium
Deferred findings
18e760af06fc873emajor performance —server/http.ts— uWS mirrors have independent read admission estimates — NOT FILED5e8b64305898b2d1major correctness —server/http.ts— Shed preflight remains a browser network error — NOT FILED6e170c48d79d6c1dmajor reliability —server/http.ts— Skipped disconnected reads have no request accounting — NOT FILED5d39cb90e728837anit correctness —server/http.ts— Array header elements crash the worker; false emits Allow-Headers: false — NOT FILEDd08479855e014d3dnit test-coverage —integrationTests/server/request-queue.test.ts— Wire-level 503 tests do not assert Access-Control-Allow-Headers — NOT FILED6ad4484803b1c948nit correctness —server/http.ts— 503 CORS config is read once at listener creation and no longer follows a config publish — NOT FILEDed0003620287d468nit test-coverage —unitTests/server/httpRequestQueue.test.js— No test exercises an operations-api listener with http_readRequestQueueLimit false — NOT FILED🤔 defer this PR's major performance finding
18e760af06fc873e(uWS mirrors have independent read admission estimates) to a separate issue instead of fixing it here?🤔 defer this PR's major correctness finding
5e8b64305898b2d1(Shed preflight remains a browser network error) to a separate issue instead of fixing it here?🤔 defer this PR's major reliability finding
6e170c48d79d6c1d(Skipped disconnected reads have no request accounting) to a separate issue instead of fixing it here?Late Grok findings
6 surviving (highest nit) — Grok reviewed c977f65, ruled against a1a2f37
validation/configValidator.ts:322—readRequestQueueLimit: 0passes validation and rejects every read while reads keep arriving. The schema description now documents this, but the behavior is unchangedserver/http.ts:770— the operations-API 503 reads allow-headers from thehttp_key. Disputed:security/auth.ts:148does the same and no operations-API key existsconfig-root.schema.json:122— the header schema allows only string, array or null, but the validator atvalidation/configValidator.ts:311-321also accepts numbers andfalseconfig-root.schema.json:140— thereadRequestQueueLimitdefault is still missing fromstatic/defaultConfig.yamlserver/http.ts:765—readRejectionHeadersstill re-implements the Allow-Origin, Allow-Credentials, Allow-Methods and Allow-Headers block fromsecurity/auth.ts:145-150. Only the origin check is now sharedDESIGN.md:162— two existingthrottle.tsindex entries are still joined with·on one lineNot standing: 1 resolved. Grok ran 20m (21m from launch to report); the fold adjudication (
cursor-grok@c977f65aef09-406f72ef, claude) took 1m.Review-Coverage: authored=gemini; ran=muse,claude,cursor-composer,cursor-muse,codex,cursor-grok; late=cursor-grok:adjudicated; adjudicated=domain; declined=cursor-kimi; rounds=10; full=3 @ a1a2f37
Review-Attention: study ~15m (hot: http.ts; decisions: do-less-alternative, read-shed-default-on, zero-limit-semantics, preflight-shed, release-target-vs-evidence, deferred-18e760af, deferred-5e8b6430, deferred-6e170c48) @ a1a2f37