feat: add zstd support for http ingress and upstream connectors - #348
Open
mxssl wants to merge 1 commit into
Open
feat: add zstd support for http ingress and upstream connectors#348mxssl wants to merge 1 commit into
mxssl wants to merge 1 commit into
Conversation
Both edges only ever spoke gzip. Adds zstd alongside it: the ingress negotiates a coding per client, and the connectors offer zstd to nodes and decode whatever comes back. Introduces internal/compression, shared by both edges, holding the q-value negotiation and the pooled codecs. On the upstream side Go's transport used to negotiate gzip on its own, which is the only coding it knows. Offering zstd means setting Accept-Encoding explicitly, which turns that off, so the connector now owns decoding for both codings. Also drops Content-Encoding from a decompressed request, which was being forwarded to upstreams describing a body that is no longer compressed, and makes the CORS handler add to Vary rather than replace it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
nodecore only ever spoke gzip on either edge. This adds zstd alongside it: the client-facing ingress negotiates a coding per request, and the upstream connectors offer zstd to nodes and decode whatever those nodes answer with. gzip and identity behave exactly as before.
zstd is both denser and much faster to decode than gzip, and node providers increasingly serve it. Against a live upstream that supports it, a 574KB
eth_getBlockByNumberresponse goes out to the client as 114KB of zstd versus 137KB of gzip — 17% smaller, at a lower CPU cost per byte.internal/compression(new)One package shared by both edges, so the codec pools exist once rather than per edge:
Negotiate(acceptEncoding)— RFC 9110 §12.5.3 q-value parsing. Highest q wins, zstd breaks a tie,q=0is a refusal, anything unrecognised falls back to identity.Offer— thezstd, gzipnodecore advertises upstream.WrapReader/AcquireWriter— pooled encoders and decoders. Levels are pinned at the fastest setting of each codec, since a proxy pays compression on the critical path of every request, where CPU costs more than the last few percent of ratio.Two limits worth calling out: encoders run at concurrency 1 (the library default spawns GOMAXPROCS goroutines per encoder, which at proxy concurrency is a goroutine count nobody asked for), and decoders cap the zstd window at 64MB (the library default is 64GB, so a hostile frame header can otherwise make you allocate on demand).
Ingress
compress.gois generalized over codecs. It is re-ported from echo's current middleware rather than patched in place, because our copy predates a fix worth having: the status line is now held until the first body byte, so an empty response no longer ships a staleContent-Encoding. Echo'sMinLengthbuffering is deliberately left out, and theLevel/Skipperconfig went with the pool.decompress.goreplacesmiddleware.Decompress(), which only ever understood gzip. It decodes gzip and zstd request bodies, passes unknown codings through untouched, and answers 400 for a body that isn't the coding it claims to be. To keep that symmetric between codings,WrapReaderchecks the zstd frame magic eagerly — gzip already validated its header on reset, so zstd failing lazily mid-read would have been the odd one out.Upstream
Go's transport negotiates gzip on its own, but gzip is the only coding it knows, and it does so only while no
Accept-Encodingis set. Offering zstd means setting the header explicitly, which turns that machinery off — so the connector now owns decoding for both codings.applyConfigHeaderssendsAccept-Encoding: zstd, gzip, unless connector config pins one — an operator who pinned it has a reason, most likely a node that mishandles a coding.dispatchdecodes before anything reads the body, so the buffered and streaming paths both see plain bytes.decodedBodyties the pooled codec's lifetime to the response body so neither path has to remember there are two things to close.Content-Encoding, so those bytes would arrive unlabelled and unreadable.Two adjacent fixes
Content-Encodingon decompressed requests. It was left on the request after decoding and forwarded upstream, telling a node to decompress a body that is already plain. The middleware now deletes it.CORS clobbering
Vary.setCorsHeadersdidSet("Vary", "Origin"), wiping theVary: Accept-Encodingthe compression middleware had just added. With two codings negotiable that is exactly how a shared cache ends up handing a zstd body to a gzip-only client. Changed toAdd.Compatibility
No new config. Clients and upstreams that don't mention zstd see byte-identical behavior. One existing test changed contract:
TestRestRequest_HopByHopClientHeadersNotForwardedasserted the upstream sees noAccept-Encoding, which is now the connector's own offer — the client's value still cannot survive, and the test says so explicitly.gRPC and WebSocket are untouched: gRPC has its own compressor registry where zstd isn't a registered encoding, and WebSocket uses permessage-deflate. Neither speaks
Content-Encoding.Testing
~20 new cases across 4 test files, written test-first. Full suite green (
go testexit 0 over 93 packages),-raceclean on the three touched packages,go vetclean.Verified against a running binary, not just tests:
zstdand forgzip, zstd, gzip forgzip, plain for no header, and every variant decoded to the same block.Accept-Encoding: zstd, gzipand received an 85-byte plain, unlabelled body even when the client sent a compressed one.gzipandidentityreached the node verbatim and both decoded correctly, with the client still getting zstd.Content-Encodingon it.