Skip to content

chore(release): version packages - #596

Open
github-actions[bot] wants to merge 1 commit into
developfrom
changeset-release/develop
Open

github-actions[bot] wants to merge 1 commit into
developfrom
changeset-release/develop

Conversation

@github-actions

@github-actions github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and publish to npm yourself or setup this action to publish automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to develop, this PR will be updated.

Releases

@derodero24/comprs@2.1.0

Minor Changes

  • 0ef61e6: Give browsers the *Async functions and @derodero24/comprs/streams, so
    that code written for the native addon runs there too. In 2.0.x, the types
    of the browser condition declared the *Async functions, but the browser
    entry did not export them, and @derodero24/comprs/streams loaded the
    native addon even in browser builds. The browser entry now exports all 23
    *Async functions, and its declarations declare them. A browser has no
    thread pool to run them on: each one runs its synchronous function on the
    calling thread before it returns, and returns a Promise of the result,
    which rejects on any error, including an invalid argument.
    @derodero24/comprs/streams now has a browser condition for imports,
    which resolves to the same Web Streams helpers on the WebAssembly build,
    with their own declarations; require() keeps loading the native addon.
    @derodero24/comprs/node remains for Node.js only.

  • b5fee8a: Add an optional maxOutputSize argument to decompress() and
    decompressAsync(), in the native addon and the WASM build. Like the
    maxOutputSize of createDecompressStream(), it limits the decompressed
    size in bytes whatever the detected format, and it defaults to 256 MB, so
    code that auto-detects the format no longer has to call detectFormat()
    and dispatch to the *DecompressWithCapacity() functions to set another
    limit. A second argument used to be silently ignored and is now the limit,
    so code that passes one by accident, such as buffers.map(decompress),
    which passes the array index, must call buffers.map((data) => decompress(data)) instead. Values that are not integers from 0 to
    Number.MAX_SAFE_INTEGER throw.

  • 9ddb3cd: Stop building and publishing @derodero24/comprs-wasm32-wasi, the WASI
    build of the native addon; 2.0.2 is its last version. Since 2.0.2 it was no
    longer an optional dependency of @derodero24/comprs, so it was only used
    where it had been installed by hand. Node.js, Deno and Bun keep loading the
    native addon for their platform. The wasm-bindgen build, which the
    package's browser entry loads, is now the only WebAssembly build.

    If you installed @derodero24/comprs-wasm32-wasi or set
    NAPI_RS_FORCE_WASI, uninstall the package and unset the variable: on a
    supported platform the native addon loads instead, and in browsers the
    wasm-bindgen build. The generated loader still looks for the WASI package
    when no native binary loads or when NAPI_RS_FORCE_WASI is true or
    error, so a copy left in node_modules would be loaded with a newer
    @derodero24/comprs whose API it does not match. On a platform without a
    native binary, loading comprs in Node.js, Deno or Bun throws Cannot find native binding; if you relied on the WASI build there, stay on
    @derodero24/comprs 2.0.x.

  • c40243c: Report the native memory of the stream contexts to V8 and allow releasing
    it early. A context keeps its encoder or decoder state outside the
    JavaScript heap, from a few hundred kilobytes for gzip to about 90 MB for
    zstd at level 19, but V8 saw only a small object and had no reason to
    collect it. A server that dropped contexts, such as the streams of aborted
    responses, could grow to gigabytes before an unrelated garbage collection
    freed them. The contexts now report their memory, so that V8 collects
    abandoned contexts in time.

    Every context class gains a close() method that releases the native state
    right away; later calls throw <format> stream already closed. Contexts are
    also disposable, so using ctx = new ZstdCompressContext() closes the
    context at the end of the scope. finish() releases the state too, and
    Lz4DecompressContext gains the finish() that the other contexts have.
    The Web streams and Node.js Transforms close their context when they end,
    fail, or are cancelled or destroyed. The contexts of the browser build gain
    close() and Lz4DecompressContext.finish() as well.

Patch Changes

  • 80da8a5: Reject the Promise of the *Async functions on invalid arguments instead of
    throwing synchronously. An invalid level, quality, capacity,
    maxOutputSize or maxDictSize, and an argument of the wrong type, such as
    a string instead of a Buffer, used to throw before any Promise existed, so
    promise.catch() missed the error. Every *Async function now returns a
    Promise that rejects with the error that the synchronous function throws
    for the same arguments, with the same message and code. Code that awaits
    the call inside try/catch is unaffected; code that relied on a
    synchronous throw from a call it does not await now gets a rejection
    instead.

  • b5fee8a: Detect every supported format reliably in detectFormat(), decompress(),
    decompressAsync(), createDecompressStream() and
    createDecompressTransform(). The empty brotli stream that
    brotliCompress() writes for empty input, zstd and LZ4 frames that follow
    skippable frames, and LZ4 legacy frames (lz4 -l) used to be reported as
    'unknown'; they are now detected. The auto-detecting streams decided on
    the format after 4 bytes and failed on brotli input that arrived in small
    chunks; they now buffer the input until the format is detected, up to
    64 KiB.

    Brotli, which has no magic number, is now recognized by decoding up to the
    first 64 KiB of the input instead of a single byte. Random data shorter than
    64 KiB and raw deflate, some of which used to be reported as 'brotli', are
    now 'unknown', and so is data that continues after the end of a brotli
    stream shorter than 64 KiB, which decompress() used to decompress while
    ignoring the rest. When data detected as brotli does not decode, including
    a truncated brotli stream, decompress() throws the unable to detect compression format error instead of a brotli error. That error now also
    points to deflateDecompress() for raw deflate.

  • 74cece7: Enforce maxOutputSize while streaming decompression runs instead of after
    each chunk. The gzip, deflate and brotli decompression streams and contexts
    used to inflate a whole input chunk before checking the limit, so one small,
    highly compressed chunk could allocate gigabytes before the size-limit error.
    They now stop as soon as the output would exceed the limit, keeping memory
    near maxOutputSize. finish() on GzipDecompressContext and
    DeflateDecompressContext now counts its output toward the limit too.

    Reject truncated input instead of returning a shorter result. The zstd,
    brotli and raw deflate decompression streams, DeflateDecompressContext and
    the deflateDecompress*() functions used to succeed with whatever had been
    decoded when the input ended mid-stream; they now throw <format> stream is truncated: unexpected end of input. The zstd and brotli decompression
    contexts gain a finish() method that performs this check, and their
    streams, including the auto-detecting ones, call it when their input ends.

    Empty input now throws for every format, in the one-shot functions and in
    the streams alike, because no format has a valid zero-length encoding.
    zstdDecompress*(), deflateDecompress*(), lz4Decompress*() and most
    decompression streams used to return an empty result for it.

  • 59055c4: Work around two bugs in the brotli 9.0.0 encoder that brotliCompressWithDict(),
    brotliCompressWithDictAsync() and BrotliCompressDictContext hit for some
    inputs. At qualities 2 to 9, including the default, the encoder panicked,
    which aborted the Node.js process; at qualities 10 and 11 it returned a
    stream that brotliDecompressWithDict() rejected with "Invalid Data". When
    the encoder panics, or when quality 10 or 11 output does not decode back to
    the input, comprs now compresses that input without the dictionary instead.
    The result is a valid brotli stream that decodes with or without the
    dictionary, only less compressed in these cases. In the WebAssembly build,
    where panics cannot be caught, the panic still traps until brotli is fixed.

  • 118ed81: Reject Large Window Brotli streams in every brotli decompression function,
    stream context and decompress(), as RFC 7932 decoders and Node.js zlib
    do. Their header declares a window of up to 1 GiB, and the decoder
    reserved a buffer of that size before writing any output, whatever the
    output limit: 12 bytes of input grew the memory of the WebAssembly build
    to 1.5 GiB, which it never returns. comprs does not write these streams;
    only encoders with the extension explicitly enabled do.

  • 694894d: The one-shot brotli decompression functions (brotliDecompress(),
    brotliDecompressWithDict(), their WithCapacity and Async variants,
    and decompress() and decompressAsync() on brotli data) hand the decoder
    the whole input at once. Small brotli decompressions no longer allocate a
    4 MiB ring buffer: 10 KB of incompressible data decompresses about 40 times
    faster, and 1 MB of incompressible data about twice as fast.

  • 6e5f891: Make the browser entry work with bundlers. In 2.0.x, browser builds of
    import … from '@derodero24/comprs' failed with esbuild and webpack, and
    Vite produced a bundle that threw on the first call: resolvers picked the
    Node.js entry before the browser one, webpack refused to parse the
    browser files as ES modules, "sideEffects": false let bundlers drop the
    WebAssembly initialisation, esbuild could not bundle the .wasm import,
    and the 2.0.2 browser entry imported the WASI package, which 2.0.2 does not
    install.

    Imports under the browser condition now resolve to the WebAssembly build,
    ahead of the Node.js entry, with type declarations that describe it rather
    than the native addon. The browser files moved to a browser/ directory
    that declares "type": "module", and the entry instantiates the
    WebAssembly module with top-level await, from
    new URL('./comprs-wasm_bg.wasm', import.meta.url): the functions are
    ready once the import resolves, with no init call. Bundlers must support
    top-level await. webpack 5 and Vite emit the .wasm file as an asset;
    with esbuild, copy browser/comprs-wasm_bg.wasm next to the bundle. On
    Vite 7 and older, vite dev needs
    optimizeDeps: { exclude: ['@derodero24/comprs'] }, and before Vite 7,
    vite build needs build.target: 'es2022'.

    require() cannot load a module that uses top-level await, so
    require('@derodero24/comprs') keeps loading the native addon even where
    the browser condition is set, as in Jest with a jsdom environment. Under
    Jest's ES module support, that environment now imports the WebAssembly
    build, which does not load in Jest; set
    testEnvironmentOptions: { customExportConditions: ['node', 'node-addons'] }
    to keep the native addon. The top-level browser files (browser-entry.js,
    browser.js, browser-streaming.js and comprs-wasm*) and the browser
    field of package.json, which pointed at browser-entry.js, are gone.

  • 89a005b: Accept any ArrayBuffer or ArrayBufferView chunk, and a
    SharedArrayBuffer as well, in the browser build of the Web streams that
    @derodero24/comprs/streams provides, as the streams of the native addon
    now do. A bare ArrayBuffer or SharedArrayBuffer used to fail, and
    createDecompressStream() read a DataView as empty and a Uint16Array
    element by element, so that it failed to detect the format; it could also
    keep a view of an ArrayBuffer chunk, which the writer might overwrite,
    while it waited for enough input to detect the format. Every chunk is now
    read byte for byte, and a chunk that is not binary data errors the stream
    with a TypeError. The browser typings accept these chunks too.

  • 0ef61e6: Make the stream contexts of the browser entry stream. They were JavaScript
    stand-ins that kept every chunk until the end and then ran the one-shot
    function: they held the whole input in memory, kept a view of each chunk
    rather than a copy, so a caller that reused its buffer compressed
    overwritten data, returned nothing from flush() (or, when decompressing
    zstd, brotli or LZ4, ended the stream there), reported corrupt input only
    at the end, and passed a zstd maxOutputSize on as a capacity that grew
    the WebAssembly memory to that size. The browser entry now exports the
    stream contexts of the WebAssembly build, which work like the native
    ones: transform() and flush() return output as soon as it is ready,
    so code that kept only what finish() returned must keep what every call
    returns, as on Node.js. Like those, they have close(), which
    [Symbol.dispose]() calls, and Lz4DecompressContext.finish(); they
    also have a free() method, which frees the context object and its
    WebAssembly memory before garbage collection does.

  • e939fc2: Derive the ES module entry from the CommonJS one. index.mjs listed every
    export by hand and has drifted from the native addon before; it now
    re-exports the CommonJS loader and the stream helpers with export *, so
    the two entries cannot drift apart. Node.js and Deno see the same exports
    as before. Bundlers now follow the entry into the loader and bundle it,
    instead of leaving a require('./index.js') that fails once the bundle
    moves; as before, the native addon itself must stay external. Bun and
    bundlers read the loader's names at run time, so their view of the package
    root now also lists the *Task classes that require() has always
    exported; they are not part of the API.

  • e939fc2: Fix the type declarations of the ES module entry. index.d.mts re-exported
    declaration files, which TypeScript rejects with TS2846, so ES module
    projects that type-check their dependencies (skipLibCheck: false) got two
    errors from node_modules. It now re-exports ./index.js and
    ./streams.js, and the exported types are unchanged.

  • 0f15e49: Reject gzip header filenames that cannot be stored. gzipCompressWithHeader()
    with a filename that contains a NUL character ('\u0000') aborted the
    Node.js process, and trapped with RuntimeError: unreachable in the WASM
    build. It now throws gzip filename must not contain NUL characters. A
    filename longer than 65535 bytes in UTF-8 was written, but gzipDecompress()
    and gzipReadHeader() could not read the result back; it now throws gzip filename must be at most 65535 bytes long.

  • 15b4d0b: Stop gzip decompression from reserving memory because of a forged size
    trailer. gzipDecompress(), gzipDecompressWithCapacity(), their async
    variants and decompress() sized their initial output buffer from the
    ISIZE trailer, which nothing verifies until decoding ends, so a few bytes of
    input could reserve up to 4 GiB, capped only by the output limit (256 MB by
    default). The initial buffer is now also capped by what the input can
    expand to and by 64 MiB, and larger outputs grow the buffer as they decode.
    gzip, brotli and LZ4 decompression also report a failed reservation of the
    initial output buffer as an error instead of aborting the process.

  • 3d7ddef: Write a content checksum in LZ4 frames. lz4Compress(),
    lz4CompressAsync(), Lz4CompressContext and the LZ4 compression streams
    now add an xxHash32 of the data to each frame, as the lz4 CLI does by
    default, so decompression detects corrupted data instead of returning it.
    The output is still a standard LZ4 frame that any LZ4 decoder reads, 4 bytes
    longer than before, and its FLG byte changes from 0x60 to 0x64.
    Computing the checksum makes compression about 15% slower, and verifying it
    makes decompressing these frames up to about 30% slower. Frames without a
    checksum, as other encoders may write them, still decode.

  • 3d7ddef: Decode every frame of LZ4 input and reject truncated or trailing data.
    lz4Decompress(), lz4DecompressWithCapacity(), their async variants,
    decompress(), Lz4DecompressContext and the LZ4 decompression streams
    used to stop at the end of the first frame, or at the first empty block, and
    silently drop whatever followed, and they accepted a frame cut short at a
    block boundary. They now decode concatenated frames like the lz4 CLI.
    Input that ends inside a frame, including a frame without its end mark,
    throws lz4 stream is truncated: unexpected end of input, and a frame
    followed by data that is not a frame throws an unexpected data after the end of a frame error. The output limit (capacity, maxOutputSize) covers
    all frames together.

    The LZ4-specific functions, context and streams also skip skippable frames,
    which used to fail with SkippableFrame, and accept a legacy frame
    (lz4 -l) followed by further frames, or with blocks of data that does not
    compress, which used to fail with BlockTooBig. decompress() and the
    auto-detecting streams still recognise LZ4 input only by the standard frame
    magic number, so input that starts with a skippable or legacy frame needs
    lz4Decompress().

  • 89a005b: Push the output of the Node.js transforms from @derodero24/comprs/node in
    chunks of at most readableHighWaterMark bytes (64 KiB by default). A small
    input chunk that decompressed to many megabytes used to arrive as one chunk
    of that size, which downstream consumers had to take in at once. The chunks
    are views of the native output, not copies.

  • 694894d: Buffers returned by the one-shot compression and decompression functions
    no longer retain an allocation sized from the input. The encoders reserved
    an output buffer as large as the input, and the decoders grew theirs by
    doubling; the whole allocation lived as long as the returned Buffer, so 50
    retained brotliCompress() results of an 8 MB input held about 400 MB of
    memory for less than 1 KB of output. Results now release large spare
    capacity before they are returned. The LZ4 compression streams also stop
    copying each output chunk.

  • 89a005b: Accept any ArrayBuffer or ArrayBufferView chunk, as CompressionStream
    does, and a SharedArrayBuffer as well, in the Web streams that
    @derodero24/comprs/streams provides with the native addon (Node.js, Deno
    and Bun). A bare ArrayBuffer used to fail with Value is none of these types, and createDecompressStream() failed to detect the format of
    DataView and Uint16Array chunks, which it read as empty or element by
    element. Every chunk is now read byte for byte, and a chunk that is not
    binary data errors the stream with a TypeError. The typings accept these
    chunks and remain assignable to TransformStream<Uint8Array, Uint8Array>.

  • 54bf460: Fix a crash in Bun and Deno when a method of a native context class is
    called with an instance of another context class as this, such as
    ZstdCompressContext.prototype.transform.call(gzipContext, chunk). The
    method used the other class's native state as its own, and the process
    crashed with a segmentation fault. It now throws an InvalidArg error;
    Node.js already rejected such calls with Illegal invocation.

    The fix comes with the update of the native addon to napi 3.13.0,
    napi-derive 3.6.9 and napi-build 2.5.0, which tag every class instance
    and check the tag before a method uses it. The crates were held at 3.9.1 /
    3.5.6 / 2.3.2 for the WASI build, which is no longer published. The
    JavaScript API, the TypeScript types and the generated loader are
    unchanged.

  • 0f15e49: Reject invalid numeric arguments instead of silently converting them.
    Compression levels and qualities, the crc32() initial value and the gzip
    header mtime were converted to 32-bit integers before any check ran, so
    NaN and Infinity became 0 (no compression for gzip, deflate and brotli),
    1.9 became 1, 2 ** 32 + 1 became 1 and crc32(data, -1) used
    0xFFFFFFFF. This applied to the one-shot and async functions and to the
    stream contexts, in the native addon and in the WASM build, which also
    turned non-numeric strings and objects into 0. maxOutputSize truncated
    fractions (0.5 became a limit of 0 bytes), and a capacity of 2 ** 64
    passed validation. All of these now throw an error that names the argument
    and its accepted range, for example gzip compression level must be an integer between 0 and 9; the messages of the existing range errors changed
    to this form. capacity and maxOutputSize accept integers from 0 to
    Number.MAX_SAFE_INTEGER on every platform (the WASM build used to reject a
    capacity of 2 ** 32 or more), and a limit of 0 accepts only data that
    decompresses to nothing. maxDictSize accepts integers from 0 to 16777216.

  • 7cf02d8: Make the WebAssembly build take and return what the native addon does.
    Its gzipCompressWithHeader() took (data, level, filename, mtime)
    rather than the (data, header, level) of the published types, so
    gzipCompressWithHeader(data, { filename }, 6) failed with
    RuntimeError: memory access out of bounds. It now takes
    (data, header, level), as the native addon does; the positional form was
    never part of the published types, and calls that use it must pass the
    header as an object. gzipReadHeader() now leaves out the header fields
    that are absent, rather than setting them to null, as the native addon
    does.

    Byte array arguments are now checked as the native addon checks them:
    anything but a Uint8Array (a Buffer is one) or another ArrayBuffer
    view throws an Error. The WebAssembly build used to compress an
    ArrayBuffer as empty input and a string as one zero byte per character,
    and its stream contexts took an ArrayBuffer or an array of numbers as a
    chunk.

  • 7cf02d8: Log the message of a panic in the WebAssembly build. A panic aborts the
    call with a trap, which throws a bare RuntimeError: unreachable; the
    build now logs the panic message with console.error() first.

  • 2180aaf: Shrink the WebAssembly build that browsers load. comprs-wasm_bg.wasm is
    now compiled for size: it is 1.87 MB instead of 2.37 MB, and 14% smaller to
    download with gzip (792 KB at level 9) and 12% smaller with brotli (545 KB
    at quality 11). In exchange, brotli compression runs about 40% slower in
    the browser, brotli decompression about 30%, gzip decompression about 20%
    and LZ4 compression about 10% slower; zstd, gzip compression and LZ4
    decompression keep their speed. The native addon is unchanged.

  • bae7130: Link the C runtime statically into the Windows ARM64 binary
    (@derodero24/comprs-win32-arm64-msvc), as the x64 one already did. Up to
    2.0.2 it imported VCRUNTIME140.dll and the api-ms-win-crt-* DLLs, so
    loading comprs failed on Windows on Arm machines without the Visual C++
    Redistributable for ARM64. It now loads without it.

  • 15b4d0b: Size zstd one-shot decompression output from the data instead of reserving
    it up front. zstdDecompress(), zstdDecompressWithDict(), their async
    variants and decompress() reserved 256 MB for every frame without a
    content size, which streaming encoders such as ZstdCompressContext write,
    and the *WithCapacity() variants allocated the whole capacity, so a value
    such as 2 ** 40 aborted the process. The output buffer now grows with the
    decompressed data: frames that declare their size still decode straight
    into a buffer of that size, as long as the input could actually expand to
    it, and everything else goes through the streaming decoder. capacity is
    only a limit, and output over it throws zstd decompress exceeded maximum size of <capacity> bytes like the other formats, instead of Destination buffer is too small.

    The one-shot zstd functions now accept concatenated frames and skippable
    frames, as the streaming API already did, and report truncated input as
    zstd stream is truncated: unexpected end of input; data after the last
    frame now usually reports Unknown frame descriptor instead of Src size is incorrect. Like the streaming API and zstd -d, they reject a frame
    without a content size whose window exceeds 128 MiB (written by
    zstd --long=28 or higher on piped input) with Frame requires too much memory for decoding.

  • 15b4d0b: Reject a maxDictSize above 16 MiB (16777216 bytes) in
    zstdTrainDictionary() and zstdTrainDictionaryAsync(). The value was
    allocated up front, so one that could not be allocated, such as 2 ** 40,
    aborted the process (a trap in the WASM build). zstd recommends
    dictionaries of about 100 KB, and training allocates several buffers of
    maxDictSize bytes.

  • 8f6967f: Speed up small one-shot zstd calls. zstdCompress(), zstdDecompress(),
    zstdDecompressWithCapacity(), their *Async variants, and decompress()
    and decompressAsync() for zstd input reuse one compression and one
    decompression context per thread instead of creating one per call:
    compressing messages of about 110 bytes is about 6 times faster, and
    decompressing them about 12 times. A thread keeps a context only while it
    holds at most 8 MiB, so a large or high-level call does not leave its
    workspace behind. The output is unchanged, and the dictionary functions
    still create a context per call.

  • 8f6967f: Speed up zstd stream compression with small chunks. ZstdCompressContext,
    ZstdCompressDictContext and the zstd compression streams no longer
    zero-fill a 128 KiB output buffer on every call: 10 MiB written in 1 KiB
    chunks compresses about 10 times faster for highly compressible data and
    up to twice as fast for JSON lines. The output is unchanged.

@derodero24/comprs-middleware@1.1.0

Minor Changes

  • 4f19597: The Fastify plugin now compresses Web ReadableStream and Response
    payloads, which it used to send uncompressed. The status and headers of a
    Response are applied to the reply first, as Fastify does, so the built-in
    checks see them. string, Buffer and Uint8Array payloads are compressed
    in one call on the libuv thread pool instead of through a stream, and are
    sent with the Content-Length of the compressed body instead of chunked; if
    that compression fails, the reply is sent uncompressed and a warning is
    logged. A stream whose reply declares a Content-Length below threshold
    is no longer compressed.

    Add the shouldCompress(request, reply) option, which receives the Fastify
    request and reply and so sees the headers set with reply.header() or
    reply.type(). filter(req, res) keeps receiving request.raw and
    reply.raw, which do not have those headers yet when it runs. A route can
    opt out with config: { compress: false }, which the plugin adds to
    Fastify's route config type. The plugin is now wrapped with fastify-plugin
    and named @derodero24/comprs-middleware, so other plugins can list it in
    their dependencies, and Fastify checks that it runs on Fastify 5.
    fastify-plugin becomes a dependency of the package.

  • af30a69: Follow RFC 9110 in all adapters. deflate responses now use the zlib format
    (RFC 1950) that the deflate content coding requires, instead of raw DEFLATE
    that strict clients such as zlib.inflateSync() reject. Accept-Encoding is
    parsed case-insensitively, including the q parameter, and an element whose
    weight is not a valid qvalue is ignored instead of read as q=1; the
    server's encodings order still decides among the encodings the client
    accepts. Vary: Accept-Encoding is added to every response that could be
    compressed, also for HEAD requests and requests without Accept-Encoding,
    and to no other response. As the filter takes part in that decision, it is
    now also called for requests that do not get compressed. Responses with
    status 204, 304 or 206, with a Content-Range header, or with an empty body
    are no longer compressed, and compressed responses get a weak ETag.
    text/event-stream responses are no longer compressed, so Server-Sent Events
    are not held back. The Hono adapter no longer keeps a Content-Length set by
    the handler on a compressed response, and the Fastify adapter no longer
    responds with 500 when a route sets Vary or Cache-Control as an array.

    The options are now checked when the middleware is created: an empty or
    unsupported encodings list, a level that is not an integer in its range or
    that names an unknown encoding, a threshold that is not a finite number of 0
    or more, or a filter that is not a function throws a TypeError or
    RangeError instead of failing on each request. zstd levels 20 to 22 are
    rejected, because their window exceeds the 8 MiB that RFC 9659 allows for
    HTTP and Chromium refuses such responses. encodings and the preferred
    argument of negotiate() accept readonly arrays.

Patch Changes

  • 2429445: Fix the Express adapter for handlers that send the response headers before
    the body. The adapter now decides whether to compress when the headers are
    emitted, so res.writeHead() and res.flushHeaders() no longer make the
    response fail with ERR_HTTP_HEADERS_SENT. Compressed responses honor
    backpressure: res.write() returns false while the client is slow and
    'drain' follows, so stream.pipe(res) pauses instead of buffering the whole
    body. A compressor error after the headers were sent aborts the response
    instead of throwing an uncaught exception, and a client disconnect releases
    the compressor. Cache-Control values set as arrays are checked for
    no-transform. Writes after res.end() fail as they do on a plain
    ServerResponse, passing ERR_STREAM_WRITE_AFTER_END to the callback and
    emitting it as 'error', and res.end() callbacks run once the response has
    finished.

    Deciding at header emission also changes three details of the Express
    adapter: a bare res.end() counts as an empty body for threshold instead
    of producing an empty compressed stream, responses with status 204 or 304 are
    no longer compressed, and Vary: Accept-Encoding is only added to responses
    that could be compressed.

  • dd3f2b6: The Hono middleware now compresses streamed responses while they are sent,
    instead of reading the whole body first. A stream() or streamText()
    response reaches the client as the handler writes it, and one that never ends
    no longer hangs; the body is read only as fast as the client takes it, and is
    no longer held twice through Response.clone(). A body that is available at
    once, such as that of c.text() or c.json(), is compressed in one call on
    the libuv thread pool instead of on the event loop. Errors are no longer
    swallowed: one that occurs before the response is sent, such as a failed
    compression or a body stream that fails right away, goes to the app's error
    handler, and a body stream that fails later aborts the response. The README
    now states that the Hono middleware runs on Node.js and Bun, not on edge
    runtimes such as Cloudflare Workers.

    This also changes three details of the Hono middleware: a failed compression
    produces the error handler's response instead of the uncompressed body; a
    streamed body has no known size, so threshold only applies to it when the
    response declares a Content-Length; and a body that is available at once
    loses the Transfer-Encoding: chunked that streamText() sets, so that the
    server can send it with a Content-Length.

  • 8d84a8d: The package can now be loaded with require() as well as import. Its
    entry points were exported for import only, so require() failed with
    ERR_PACKAGE_PATH_NOT_EXPORTED, and TypeScript projects that compile to
    CommonJS reported TS2307. @derodero24/comprs-middleware/package.json is
    exported too. The package still ships ES modules only, so it now requires
    Node.js 22.12 or later instead of 22.0: 22.12 is the first Node.js 22 release
    whose require() loads ES modules without a flag. A TypeScript project that
    compiles to CommonJS needs module set to nodenext or node20.

    Express is no longer a peer dependency: the Express adapter only uses the
    Node.js request and response, and works with Express 4 and 5.

    The @derodero24/comprs peer range is now ^2.0.2 (it was ^2.0.0). When
    both packages are released together, the middleware is now published only
    after @derodero24/comprs, so its peer range can always be met. The
    published manifest no longer lists workspace:* for the @derodero24/comprs
    devDependency, and the package now includes its license file.

  • 4f19597: Read Vary as a list of field names when adding Accept-Encoding to it. A
    Vary that names a field merely containing accept-encoding, such as
    X-Accept-Encoding-Hint, now gets Accept-Encoding added, which it used to
    miss, and a Vary that lists * next to other field names is left as it is.

@github-actions
github-actions Bot requested a review from derodero24 as a code owner October 3, 2026 14:15
@coderabbitai

coderabbitai Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 1bd81736-8f26-472a-ab1a-3c70c32a96fc

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions
github-actions Bot force-pushed the changeset-release/develop branch 28 times, most recently from 2693697 to 7f9f2fa Compare October 4, 2026 04:32
@github-actions
github-actions Bot force-pushed the changeset-release/develop branch 6 times, most recently from b9484bd to 313aaa4 Compare October 4, 2026 05:11
@derodero24 derodero24 mentioned this pull request Oct 4, 2026
41 of 62 tasks
@github-actions
github-actions Bot force-pushed the changeset-release/develop branch from 313aaa4 to de28313 Compare October 4, 2026 05:13

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants