feat(effect): add HttpMiddleware.compression for gzip/deflate response compression#2151
Draft
kitlangton wants to merge 3 commits into
Draft
feat(effect): add HttpMiddleware.compression for gzip/deflate response compression#2151kitlangton wants to merge 3 commits into
kitlangton wants to merge 3 commits into
Conversation
🦋 Changeset detectedLatest commit: ce75a9c The changes in this PR will be included in the next version bump. This PR includes changesets to release 27 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
- pickCompressionEncoding now tokenizes Accept-Encoding (no more substring match — 'gzipold' won't match 'gzip') and strips q-value parameters - options.skip is now a Predicate<HttpServerRequest> for parity with cors - HttpRouter.compression infers its options type from HttpMiddleware.compression - Move stale Content-Length removal into the Stream branch where it's actually needed, instead of guarding inside applyHeaders
…on regex The compressible content-type set is consistent with IANA mime-db's `compressible: true` flags, so the comment now points to mime-db as the authority. Already-compressed formats (woff/woff2, image/png, image/jpeg, video/audio/zip) remain excluded by design. Also: - Bake `text/event-stream` exclusion into the regex with a negative lookahead so the policy is correct in isolation, not just via the runtime guard. - Drop redundant explicit `image/svg+xml` and `application/xhtml+xml` — already matched by the `+xml` suffix arm. - Add a 20-case parametrised test matrix locking the policy against mime-db, including the load-bearing exclusions of woff/woff2.
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.
Summary
Adds an HTTP response compression middleware so Effect HttpApi users don't have to roll their own. Effect-smol's
HttpMiddlewareships logging, tracing, and CORS, but no compression — Hono, Express, Fastify, and nginx all ship one.This is platform-agnostic: it uses the Web Standard
CompressionStream(available in Node 18+, Bun, Deno, and browser-style runtimes) instead ofnode:zlib, so it works anywhere effect-smol runs.What's added
HttpMiddleware.compression(options?)— middleware that conditionally compresses response bodies with gzip or deflate.HttpRouter.compression(options?)— matchingLayer<never, never, HttpRouter>registration, mirroring thecorspattern.HttpServerResponse.removeHeader— small public addition; the compression middleware uses it to clear staleContent-Lengthafter swapping in a Stream body of unknown length.Defaults (mirroring Hono's
compress)threshold: 1024 bytescompressibleContentType: regex coveringtext/*, commonapplication/*(json/xml/javascript/wasm/…),image/svg+xml,+json/+xml/+yaml/+textsuffixes, etc.text/event-streamis always excluded so SSE chunks flow immediately.encodings:["gzip", "deflate"]— gzip preferred when both are accepted.bris intentionally not included; it would need separate handling and isn't supported byCompressionStreameverywhere.skip?: (request) => boolean— user predicate (e.g., to bypass specific paths).Skip conditions
HEADrequestsContent-EncodingCache-Control: no-transformtext/event-streamContent-Length<thresholdEmpty/Raw/FormData(onlyUint8ArrayandStreamare compressed)When compressing
Stream(orUint8Arrayif the original body wasUint8Array)Content-EncodingContent-Lengthfor stream bodies (compressed length unknown)Accept-Encodinginto any existingVaryheader (preserving e.g.Vary: Originfrom CORS)ETagtoW/...Test plan
15 new unit tests covering all branches:
Uint8Arraybody with gzip; round-trip decompresses correctlyStreambody and clears staleContent-LengthAccept-Encodingis missingbronly)image/png)text/event-streamContent-EncodingCache-Control: no-transformHEADrequestsskippredicateAccept-Encodinginto existingVaryheaderETag("abc"→W/"abc")ETagunchangedpnpm check:tsgo,pnpm lint-fix,pnpm docgentest/unstable/http/tests passminor)Open questions for the maintainers
HttpMiddleware.compressionto match the existing module style; happy to rename tocompressif that's preferred.CompressionStreamdoesn't accept"br"in Node, so it would need a Node-only path. Easy follow-up.compressibleContentTypeoption is a singleRegExp(matching Hono). Open to making it a predicate(contentType) => booleanif more flexibility is wanted.