http,http2: speed up header validation and write coalescing - #65332
http,http2: speed up header validation and write coalescing#65332anonrig wants to merge 6 commits into
Conversation
|
Review requested:
|
Replace regex-based header token/value checks with byte lookup tables, cache OutgoingMessage lenient-validation, and coalesce headers with small Buffer bodies into a single socket write. Scan IncomingMessage rawHeaders for Content-Length and Transfer-Encoding so optimizeEmptyRequests does not force req.headers construction. On the HTTP/2 path, skip toLowerCase for already-lowercase names, use a Set for sensitive/single-value header checks, and reserve outgoing session storage to avoid reallocs while gathering nghttp2 frames. Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
Send headers, the last chunk, and the chunked terminator in a single write() when res.end() is used with Transfer-Encoding: chunked. Reuse prebuilt HTTP/1.1 status lines for default reason phrases, skip toLowerCase() on common outgoing header names, and hoist HTTP/2 header serialization off the per-call closure. Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
76c8e14 to
62327c4
Compare
The chunked end() fast path concatenated headers with the body and wrote the result using the body encoding. That re-encoded obs-text header values as UTF-8 and reduced corked res.end() to a single socket.write(), which broke test-http-server-non-utf8-header and test-http-response-cork. Copy headers as latin1 into the combined buffer, and accept a single write after uncork. Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
JS lookup tables were slower than V8's regex for header values and for token names longer than ~10 bytes. Restore the regex path for those cases, and use the table for names of length <= 10 so Connection / Keep-Alive stay on the faster path. Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
|
Local results from a release build of this branch vs Correctness
Micros (3 runs, ops/s) A JS lookup table for all token lengths and for header-value checks was a regression (V8 regex is faster past ~10 bytes). Latest commit keeps regex for values and for tokens longer than 10, and uses the table for
HTTP/2 So the measurable end-to-end win is the small- |
There was a problem hiding this comment.
Pull request overview
This PR optimizes Node.js HTTP/1.1 and HTTP/2 hot paths around header validation/serialization and write coalescing to reduce per-request overhead in writeHead()/end() and small-body responses.
Changes:
- HTTP/1: faster header-name token validation, cached lenient-validation, reused prebuilt status lines, avoided common-case
toLowerCase(), and added header/body write coalescing (including a combined chunkedend()write). - HTTP/1 server: avoid forcing
req.headersconstruction when checking forContent-Length/Transfer-Encodingby scanningrawHeaders. - HTTP/2: reduce lowercasing overhead in header processing and reserve outgoing send storage to reduce reallocations.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| test/parallel/test-http-response-cork.js | Updates expectations to allow fewer socket.write() calls due to new coalescing behavior. |
| test/parallel/test-http-common.js | Adds more checkInvalidHeaderChar() test cases for strict vs lenient validation. |
| test/parallel/test-http-chunked-end-coalesce.js | New test validating wire format when chunked res.end() is coalesced into a single write. |
| src/node_http2.cc | Reserves outgoing HTTP/2 storage in send path to reduce reallocations. |
| lib/internal/http2/util.js | Refactors nghttp2 header string building to reduce lowercasing/closure overhead and uses Set for strict single-value checks. |
| lib/internal/http2/core.js | Avoids unconditional toLowerCase() in response header array scanning. |
| lib/_http_server.js | Prebuilds HTTP/1.1 status lines and avoids unnecessary statusMessage validation for defaults; uses req._hasBodyHeaders() for optimizeEmptyRequests. |
| lib/_http_outgoing.js | Coalesces headers with small bodies; adds combined chunked end() fast path; caches lenient-header-validation result; reduces common-case toLowerCase(). |
| lib/_http_incoming.js | Adds IncomingMessage.prototype._hasBodyHeaders() scanning rawHeaders to avoid constructing req.headers. |
| lib/_http_common.js | Speeds up checkIsHttpToken() by using a lookup table for short names and regex for longer ones. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if (chunk) { | ||
| if (this.finished) { | ||
| onError(this, | ||
| new ERR_STREAM_WRITE_AFTER_END(), | ||
| typeof callback !== 'function' ? nop : callback); | ||
| return this; | ||
| } | ||
|
|
||
| if (this[kSocket]) { | ||
| this[kSocket].cork(); | ||
| } | ||
|
|
||
| write_(this, chunk, encoding, null, true); | ||
| write_(this, chunk, encoding, onFinish.bind(undefined, this), true); |
Pass NativeHttpHeaders from the parser instead of a JS string array. IncomingMessage materializes rawHeaders/headers only when read. Host/Expect/body-header checks use C++ has/get. Skip Buffer::Copy for dumped bodies via parser.setSkipBody(). Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
Store header name/value bytes in a packed Buffer instead of a per-request native BaseObject or JS string array. IncomingMessage materializes rawHeaders only when read. Host/Expect/body-header checks use flag bits so the default server path never creates header strings. Dumped bodies skip Buffer::Copy via setSkipBody(). Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
|
Follow-up: incoming header bytes now stay off the JS string heap until something actually reads them.
A first attempt used a
I am not updating the |
This speeds up the HTTP/1 and HTTP/2 hot paths that show up in
writeHead()/end(), small-body writes, and HTTP/2 header serialization.HTTP/1
Connection,Keep-Alive). Longer names stay on the regex path; a JS table overthose lengths is slower than V8's regex.
OutgoingMessagelenient-validation and skipstatusMessagevalidation for the built-in reason phrases.
HTTP/1.1 <code> <reason>\r\nstatus lines.toLowerCase()for the common Title-Case / lowercase spellingsof
Connection,Content-Length,Transfer-Encoding, and friends.Bufferbodies into onesocket.write().res.end(chunk)is used withTransfer-Encoding: chunkedandheaders have not been flushed, send headers + last chunk + terminator
in a single write (headers copied as latin1 so obs-text is preserved).
rawHeaders/
headersmaterialize JS strings only when read. The default serverHost / Expect / body-header checks use flag bits and do not build
req.headers.IncomingMessage._dump()/ dumped bodies skipBuffer::Copyviaparser.setSkipBody().HTTP/2
toLowerCase()when header names are already lowercase.buildNgHeaderStringprocessing off a per-call closure.Setfor sensitive / strict single-value header checks.nghttp2_session_mem_sendchunks.These are incremental, behavior-preserving changes. They do not move
end-to-end
benchmark/http/simple.jsby 50%. Localwrknumbers vsupstream/main(same machine, release build, 2x 5s) for the JS write /validation work:
check_is_http_tokenforConnectionis about +21%. Header-valuevalidation stays on regex (a JS
Uint8Arrayscan was 30–50% slower).The packed-header follow-up keeps bytes off the JS string heap until
something actually reads
rawHeaders/headers. A per-request nativeBaseObjectwas slower than creating the string array, so the bytes livein one Buffer with flag bits for Host / Expect / body headers.
Tests
checkInvalidHeaderCharcases intest/parallel/test-http-common.js.test/parallel/test-http-chunked-end-coalesce.jschecks the wireformat of coalesced chunked
end()(string, buffer, UTF-8, trailers,unusual
Content-Lengthcasing, latin1 header values).test/parallel/test-http-native-headers.jschecks lazy Host / Expectlookups and materialization on
req.headersaccess.Local: 405/405
test/parallel/test-http-*(HTTP/1) passed against arelease build of the packed-headers tree. Earlier 776/776
test-http*/test-https*/test-http2*passed on the same branchbefore the Buffer packing tweak.