Repository navigation
Conversation
vansergen
force-pushed
the
build/monorepo
branch
from
October 13, 2026 14:10
de790ec to
93e7fb4
Compare
…binden/compression The compression replaces the write methods of the response itself, so the guard of binden did not see the calls of the abandoned work (e.g. a middleware still writing after the deadline): they decided the coding, created the compressor and committed the headers under the reply of the error handler. Its entry points now ignore the work that may no longer mutate the response, as binden does, and call the callbacks once, asynchronously. The compressor keeps the operation of the work that has committed the headers, and writes its output as that owner. Once the response has been ended through it, the transfer of what it has accepted goes on even when that owner is abandoned afterwards: an error handler that times out with an ended, compressed reply under backpressure no longer leaves the client waiting for the rest of the body. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A stream checked its Content-Length whenever it read it, so once a
compression had removed the header at its first write, a stream shorter
or longer than its declared length ended with a complete chunked body.
The length is now read once, before the first write, and a mismatch
cuts the response and rejects, compressed or not.
A string sent with an encoding was measured by Buffer.byteLength(),
which only estimates the bytes of base64 (e.g. with line breaks) or of
invalid hex: the declared length did not match the body, and the next
response of the connection was read as a part of it. It is now encoded
once, and those bytes give the length, the ETag and the body.
HEAD of an empty body had no Content-Length (and a compression could
then decide otherwise than for GET), HEAD got a default Content-Type
also for 204 and 304, and send("") took another path than an empty
Buffer (no ETag). Both methods now get the same headers, with the
default Content-Type only for a status that has a body. A stream that
has already ended is an empty body (of its declared length), and one
that has been destroyed rejects at once instead of never settling. A
1xx status, which the client takes for an interim response, rejects
with a TypeError (use writeEarlyHints()).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With root, a socket inside the root could not be opened (ENXIO, or EOPNOTSUPP on macOS), which was not taken for a missing file: the request got a 500 instead of a 404. Every other kind of file was opened before its type was checked, which may have effects of its own for a device. The real path is now checked to be a regular file (without following a link) before it is opened: anything else is a 404, and the errors of a file replaced by a socket or a device between the check and the open (ENXIO, EOPNOTSUPP, ENODEV) are 404 as well. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…) in binden The parser of Content-Type drops the parameters that it cannot read, so type() accepted e.g. "text/plain; charset" or "text/plain; a=b c" and set another value than the one it was given. It now throws a TypeError unless every parameter is a token with a token or a quoted string as value (RFC 9110, section 8.3.1), with optional whitespace. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Once a response is sealed (e.g. its deadline has passed), the writes of the abandoned work are ignored, but the access log still counted their bytes in `body_bytes`. It counts a write only when the work that makes it owns the response. The error reply no longer lists `Content-Type` twice in the headers it removes: the representation headers include it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n the pack rules The review of the pack rules slipped files through them. They only read a `//#` link on the last line of a generated file, while Node.js and the browsers follow the last link wherever it is and in the `/*# … */` form too: a local path could be published behind a trailing comment. SSH keys (`id_*`), keystores (`.jks`, `.keystore`), PKCS#8 and PuTTY keys (`.p8`, `.ppk`), the credential files of netrc, PyPI and git, Go-style tests (`*_test.*`) and `__mocks__/` were not refused at all, and `Test/` or `.NPMRC` escaped the rules although they are the same names on the file systems of macOS and Windows. Every link of a generated file is now found in both comment forms and the last one is checked; the names are compared without case; the new keys, credential files, tests and mock directories are refused, whatever scripts/pack-files.json lists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… checked The release job packed the archives itself, with the rights to publish, and the archives that the lint job checked (the rehearsal, the packed files and the consumer check) were other ones, packed again from the same commit: nothing proved that npm received the bytes that were checked. A `package` job (contents: read, no npm token, no OIDC) now computes the next version as the rehearsal does, runs `version` and `pack` on Node.js 24.16.0 and npm 11.13.0, checks the archives (pack-check and consumer-check with --tarballs) and uploads them with their manifest as an artifact of the run, with the digest of the manifest as a job output. The release job installs only its tooling (npm ci --ignore-scripts), downloads that artifact by the id that the job reported (never an artifact of another run), compares the digest of its manifest, and lets semantic-release publish it: `prepareCmd` runs `release.mjs version` (manifests and lockfile, no build, no pack) and `publish --dry-run`, so the existing publisher checks the version, HEAD as the commit, the integrity, the files and the pack rules of every archive before the tag is pushed; `publishCmd` publishes these archives after its registry preflight. A recovery takes the same way, where `recover-package` checks the version, the tag and its ancestry before anything of the repository runs and packs the tagged commit, and `recover` checks them again, then publishes that artifact with --skip-existing. The rehearsal publishes its archives with --dry-run from another clone, as the release job does. The tests run the commands of .releaserc.json and semantic-release itself against the fake npm (archives of another version, of another commit, changed or substituted: nothing is tagged or published), a partial publication then its recovery from the archives of another checkout, and the wiring and the bash steps of the jobs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… CI runs `@types/node` 26 needs `skipLibCheck: true` because of the types of thread-stream (used by pino): it is now stated as a limitation of the supported setups in the known issues, the migration guide and the checklist of the production guide, next to the strict setup with `@types/node` 24 that the consumer check keeps checking. GitHub keeps one pending run per concurrency group and cancels it when another one is queued: a recovery dispatched while a run of main is in progress can be cancelled by the next push (and it cancels a run of main that waits), which the release documentation now says, with what to check. The README said that the packages require npm >=11.11.0: only the repository does, the packages declare no npm version and require only Node.js >=24.14.1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The release rehearsal, the check of the packed files and the consumer check moved from the lint job to the package job, which packs the archives that a release publishes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A compression removed the Content-Length of the body that it codes and let Node.js frame the rest. Node.js remembers every removed framing header: once binden had removed a stale Transfer-Encoding (e.g. copied from an upstream response) or the application had removed one, the compressed body of an HTTP/1.1 response was delimited by closing the connection, so that the next pipelined requests of the connection were lost, and a stale Transfer-Encoding written directly was sent in chunks to an HTTP/1.0 client, which does not know them. The compressed body is now framed explicitly: Transfer-Encoding: chunked for HTTP/1.1 (also on the headers of HEAD, as for GET), and Connection: close without a Transfer-Encoding for HTTP/1.0. Only the headers that are set are removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sendFile() and the dynamic ETag evaluated the preconditions against the body as it is, and @binden/compression then sent the identity representation to a request whose If-Match listed its strong tag, whatever the request accepted (also with identity;q=0), and a range of the file as it is to a request that accepted gzip. The decision of the compression came too late: it was taken when the headers were committed. A library can now choose the representation of a body before the core evaluates the preconditions (kNegotiator, kRepresentation, internal to the packages): the choice sends no header and starts no coding, and the commit of the headers applies it. @binden/compression registers its decision, which no longer has side effects until the commit. sendFile() and send() with the dynamic ETag choose the representation, then evaluate the preconditions with its ETag (weak once compressed: a strong If-Match fails with an empty 412, without a compressor nor Content-Encoding), then the ranges, which only the file as it is has (a compressed file is sent whole). A representation replaced with 406 keeps that status whatever the preconditions are, and the file is not read then. A stream that fails before its first chunk forgets the choice, so that the error reply (or the 503 of a deadline) gets a decision of its own. The dynamic ETag now evaluates every precondition as sendFile() does: If-Match and If-Unmodified-Since fail with 412, and If-Modified-Since gets 304 with the Last-Modified that the response may have. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…compression The compression only checked Accept-Encoding for the responses that it could compress. A fixed format was sent whatever the request accepted (also with gzip;q=0, identity), a response below the threshold, refused by the filter, skipped, with no-transform or a range was sent as it is to a request that refused identity, and an encoded response (e.g. a precompressed file) was sent whatever the request accepted. The decision now reads the acceptable codings first (RFC 9110, section 12.5.3), then whether the response may be compressed, then chooses the representation. That is the acceptable coding that it prefers among the offered ones (a fixed format is the only one offered, and identity is preferred without Accept-Encoding or with an empty one), otherwise the response as it is when identity is acceptable, otherwise 406. An encoded response has no other version, so it is sent as it is when the request accepts its coding (any coding without Accept-Encoding, none with an empty one), and gets 406 otherwise. A response without a body (1xx, 204, 205, or an empty body) never gets 406. Every response with a body gets the header Vary with Accept-Encoding, as each depends on it. The empty 412 of a failed precondition (of sendFile() and of the dynamic ETag) no longer carries the Content-Encoding of a precompressed body, which an empty body does not have; its 304 keeps it. BREAKING CHANGE: a fixed format is used only when the request accepts it (send Accept-Encoding, e.g. from the proxy that negotiates); a request that refuses identity gets 406 also for the responses that are not compressed, and a precompressed response gets 406 when the request does not accept its coding. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pression The review reproduced four representations that the request had refused: a fixed gzip format sent to gzip;q=0, identity; a body below the threshold and one refused by the filter sent as they are to identity;q=0; and a file whose strong validator met If-Match sent as it is to gzip, identity;q=0. One test now replays them as they were reported, for GET and HEAD: identity, 406, 406 and 412. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…compression The bullet about the 304 of a compressed response was wrapped in the middle of a sentence, and the comment of the decision said that every response with a body depends on Accept-Encoding, which the 500 of a failed filter does not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The type of the `state` factory refuses a promise through a `then` property, which had no comment: `npm run docs:build`, which refuses an undocumented export, failed on it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool. What Enabling Code Scanning Means:
For more information about GitHub Code Scanning, check out the documentation. |
… binden CodeQL found polynomial regular expressions in three parsers of request headers: quantifiers that can match the same spaces or tabs backtracked on long runs of them (about 350 ms of CPU for one header of 15 KB with Authorization or Range). The scheme of `Authorization` and the weight of `Accept-Encoding` are split at their first space or `=`, and the elements of `Range` are trimmed in linear time before a regular expression without overlapping quantifiers matches them. A test bounds the time of the inputs that CodeQL reported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test of `createSecureServer()` disabled the verification of certificates (CodeQL), because the certificate of the tests named another host. It is replaced by a certificate for `localhost` (and 127.0.0.1, ::1), which the client trusts as its own authority: a certificate for another name, or one that is not trusted, now fails the test, which is bounded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…form On GitHub, the tests failed where the platform differs from the machine they were written on: - Node.js 24.14.1 has no `writeInformation()`: the test of the override was skipped, so its lines were not covered. A test now checks that, without it, an abandoned call is ignored and an allowed one throws a TypeError, as Node.js itself would. - Windows takes a whole write while its send buffer has room: three tests that need a response that cannot finish before the client reads it are skipped there, like the others of the kind. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Windows job failed on three tests of behaviours that Windows does not have, which the other
platforms still check:
- the SIGTERM shutdown of binden: `kill("SIGTERM")` ends a process at once on Windows;
- the end of a coding in @binden/body: Windows resets a connection closed with unread data at
once, so the client may lose the reply;
- the replaced target of a failed copy in @binden/multipart: Windows refuses to rename a file
that is open, so the replacement cannot be staged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Windows, Node.js writes an uncaught error with CRLF line endings, and the git of the release script warned about CRLF conversions: three tests of scripts/release.mjs did not find the messages they expected there. The tests normalize the line endings of its stderr, and the fixture repository turns `core.autocrlf` off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… tests in the pack rules The second review of the pack rules slipped files through them. Only the last link of a generated file was checked, while V8 follows the last COMMENT: a string or a template literal that looks like a relative link hid an absolute `//# sourceMappingURL=` comment before it, and a link followed by another token on its line (which V8 ignores) passed as well. Only `.js` and `.d.ts` files were read, although Node.js runs TypeScript sources and bundlers and browsers read `.jsx` or `.css` links, and the `file` of a map could keep the absolute path of the build. Common credential files (`_netrc`, `.yarnrc.yml`, `.aws/credentials`, `.pgpass`, `.htpasswd`, `.envrc`, `credentials.json`, `.docker/config.json`, `.kube/config`), keys (`.asc`, `.gpg`, `.pkcs12`, `.p7b`, `.kdbx`, `.ovpn`), tests (`*-test.*`, `test.*`, `*.e2e.*`, `spec/`, `e2e/`, `__fixtures__/`, `fixture/`, `testdata/`) and mocks (`mocks/`, `*.mock.*`) were not refused, while the SSH key rule (`id_*`) refused modules such as `dist/id_token.js`. Every link of every published file is now checked, in both comment forms, and its whole value (to the end of the line or of the comment) must be one relative path to a published map; the `file` of a map must be relative to it; the new names are refused without case, and the SSH rule matches the key names only (`id_rsa`, `id_dsa`, `id_ecdsa`, `id_ed25519`, with `_sk` and `.pub`). The tests replay every case of the review, and V8 itself confirms which link it takes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on wrote The publisher checked the name, the version and the gitHead of the package.json inside each archive, but nothing else of it: an archive whose package.json gained a postinstall script or a dependency, listed in a consistent manifest with its integrity and digest, was published (case S5 of the review), although npm installs and runs what that file declares. `publish` now compares the package.json of every archive byte for byte with the one that `scripts/release.mjs version` wrote into the workspace of its checkout, and `pack` does the same with the bytes that `version` wrote before `npm pack` ran (a `prepack` script that rewrites the manifest is refused there, not only at the release). npm packs package.json as it is on disk, so the archives of the `package` job match what the release job writes from the same commit. The fake npm keeps package.json as it is on disk, like npm, and can rewrite it like a prepack script; the tests cover a script, a dependency, a bin and other formatting of the same fields, in `pack`, `publish` (with and without --skip-existing) and the commands of .releaserc.json. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he release relies on The recovery jobs ran for a dispatch from any branch: `recover-package` and `recover` checked that the commit of the tag is on `main`, but not the ref of the run, and nothing in the repository said that the `release` environment must admit `main` only. A run dispatched from another branch runs the workflow of that branch, whose conditions may be gone, so the environment (with npm trusted publishing bound to it) is what keeps such a run from publishing. Both recovery jobs now run only for a dispatch from refs/heads/main. The workflow tests evaluate the condition of every job for pushes, pull requests and dispatches from main, another branch or a tag (each publishing job checks the ref itself, not only through the jobs it needs), and pin that only `release` and `recover` can publish to npm, both in the `release` environment. The README states the settings outside the repository: the `release` environment limited to the branch `main` with required reviewers and no administrator bypass, npm trusted publishing of the five packages bound to `ci.yml` and that environment (any NPM_TOKEN an environment secret only), the protection of `main` with its required checks (`package` among them), the former repositories archived, and the security features; the recovery is dispatched from `main`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…@binden/body The zstd decoder of Node.js 24 stops after the first frame of a write, and accepts a truncated frame. So a body of concatenated frames (valid, RFC 8878) got a 400 when its frames came in one chunk, and a truncated body was parsed as a shorter one. The parser now finds the frames itself and gives one frame at a time to a single decoder, so that concatenated and skippable frames decode the same on every supported version, and a truncated frame is a 400. One `limit` counts the content of all the frames, and `encoded_limit` and `verify_encoded` the body as received, once. A body with a coding and no byte at all is a 400 for zstd too, as for the other codings, while a frame of an empty content is an empty body. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`identity` means no coding (RFC 9110, section 8.4.1), and @binden/multipart and the parser of 2.0.1 accept it, but the parser refused `Content-Encoding: identity` with a 415. It is now left out of the list of codings, in any case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…@binden/multipart A refusal destroyed the request once the response had started, and a send of a stream that still waits for its first chunk counts as started: at the deadline the client got a closed connection instead of the 503, and the send failed. The refusal now destroys the request only once the headers are sent. Before, it leaves the send to the framework, which cancels it at the deadline and replies, or sends it with `Connection: close`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in @binden/multipart A parse that goes on after the cleanup of the request (e.g. not awaited before the reply) refused its files with "A file has not been handled", whose cause only said that the request had been completed. The disk storage now refuses them with a 500 that tells why (the cleanup has started, and would not remove them), also when the cleanup stops a file that is being written. The README ties the refusal to the cleanup, not to the end of the body. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…source in binden The guard of a source does not report an error that a send hears, since it fails that send. The stream-like adapter counted its listener, but the send of a Node.js Readable did not. A Readable sent to one response and released by another (e.g. to HEAD) whose destroy then failed rejected the live send with that error and was also logged late by the guard of the release (R1 R20-4). The listener of error of the send now counts as hearing from when it is added until its removal, also when that removal throws, and a kept listener stays counted. An emitted error that fails the send has decided it, so a guard whose listener comes after the one of the send does not report it either. A send that has been stopped by another writer reports such an error itself, once, since its guard no longer does while the send hears the source. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…at decides its send in binden A Node.js Readable whose code throws one object for every listener of its close rejected its send with that object, and the guard of the send, which the same code then refused, logged it again as a failed release (R3.20-4). A failure that binden finds itself (own) is now recorded as decided when the send settles, before its guard is installed, also when the guard is refused and the source is destroyed without it. guard() no longer gives a decided error to its refusal callback. The send of a source that has been destroyed or has errored before passes the error that it settles by to its guard, whose refusal does not log it either: the rejection, or the late report once the response has ended, tells it once. A primitive cannot be recorded, and is still logged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…le in binden A stream-like source whose code refused its guard, or threw in a pause or a removal of a listener while send() released it, was logged with the message of a late error. A Node.js Readable logs the same failures as "The release of a stream has failed" and "The removal of a listener of a stream has failed" (R2 N6a). The response now passes its own logs into readStreamLike() and releaseStreamLike() (IStreamLikeLogs), so both kinds of sources log a release alike. Those logs hold the response, and no listener of a guard holds them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… at its finish in binden A send counts as complete for the client once its declared Content-Length bytes are flushed, so a client that goes away afterwards does not fail it. For a declared length of 0 that held at once, before anything was written: a client that went away before the head left the send resolved although the headers had never been sent (R1 R20-6). An empty declared body is now complete only once the response has finished. Before that, a close rejects the send as a premature close. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g writes in binden The JSDoc said that a send whose response closes before it has settled rejects as a premature close. When writes are pending, Node.js and the system usually report the error of the connection first, through the callback of a pending write: EPIPE, ECONNRESET or ECANCELED, depending on the platform and the timing (R1 R20-3, R3.20-1). send() and #pipe() now say so: the send rejects with the error of the connection when a pending write fails first, otherwise as a premature close (or resolves once its body is complete for the client). A release of its source still never decides it. ffba1b2's claim is corrected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts close in binden The guard of a stream-like source left on the turn after its close. Sources whose state has no closeEmitted (readable-stream 2 and 3, through2 4, plain emitters) still emit errors after their close, e.g. an SSE feed written on a timer once binden has destroyed it at the deadline or once the client has gone away (ERR_STREAM_DESTROYED, "write after end"), a second destroy(error), or any error of an emitter. Without a listener of the application, the process crashed, because of binden's own cancellation; Node.js pipeline() keeps its listeners and survives the same. The guard now leaves only when hasClosed() tells that the source has closed. Otherwise it stays as long as the source lives, holding only the report of a late error, which logs each later error once. A fixture without process handlers checks the reproduced cases, and a GC fixture that the kept guards do not retain the responses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n binden A stream-like source that refuses every listener of its error (e.g. a listener of its newListener that throws) was still paused once binden could not release it. Its pause() is code of the source, which may emit an error asynchronously: with no listener in place, that crashed the process. Such a source is now left fully untouched, both when it is sent and when it is released (HEAD, 204, 304, 1xx): neither its pause() nor its destroy() runs, and the response logs that it could not be released. A source whose guard or send listens is still paused or destroyed under that listener, so an asynchronous failure of its pause is reported like one of a destroy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on a failed removal in binden At settlement, the adapter installed the guard of the source before it detached the listener of error of the send. The guard's on() runs code of the source (e.g. a listener of its newListener) that may emit an error at once: it reached the settled send, which cut a complete reply (and lost the next response of the connection) or dropped the error. The listener of the send is now detached first, so such an error is reported once as a late error. A removal of that listener that removed it and then threw left the source counted as heard: its guard stayed silent and a later error was never reported. The count now drops in a finally, whether the removal threw before or after removing the listener; one that is left is detached and reports a later error itself only when the guard does not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eciding error in binden
A destroy() of a stream-like source that threw inside the destruction hook of its readable
escaped into Node.js, which then destroyed the readable with that throw: the release failure
replaced the error that decided the send (e.g. RESUME FAILED became RELEASE FAILED). The hook
now catches it, logs it as a failed release, and completes with the deciding error.
releaseStreamLike() (HEAD, 204, 304, 1xx) called destroy() or pause() unprotected: a throw
rejected the send, so HEAD got a 500 and a 1xx send lost its TypeError. It now logs the throw
and goes on, as for a Node.js Readable.
Code of the source that throws a value whose stack getter throws made readable.destroy() throw
inside Node.js's checkError: at setup the listeners of the send stayed and a later error of the
source crashed the process, in pause() the throw escaped into its emit("data"). The readable is
now destroyed defensively, with an Error that carries such a value as its cause, so the
listeners are always released and the send rejects.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sistent in binden A stream-like source destroyed with an error before the send (its errored, e.g. of readable-stream 4) rejected as a premature close, its real error logged only as a late error. It now rejects with that error, as a Node.js Readable does, and is not destroyed again. The reason with which a cancellation destroys a stream-like source (the deadline's, the client's, or the premature close of a falsy value that its code threw) was never recorded as decided, so its guard logged the very error that the send had rejected with. It is now recorded before the destroy. When code of the source ended the response while the send read it (an errored getter, or its own on() that throws), the adapter recorded the error as decided before the send had decided on it, so the send resolved and nothing was logged. The adapter no longer decides what the response decides; the response's late report of its readable is recorded for the source itself, so it is logged once. While the send decides, no guard reports that error, neither from a destroy that emits it at once nor from a refusal of the guard's listener that throws it again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ce failed mid-send in binden A data event of a stream-like source that carried null was pushed into the adapter's readable, where push(null) is its end: the send resolved with a truncated body framed as complete, and undefined was silently dropped. A chunk that is neither a string nor bytes now destroys the readable with a TypeError (ERR_INVALID_ARG_TYPE), whatever it is, so the send rejects and a committed response is cut. A stream-like source that emitted an error mid-send without being destroyed (e.g. readable-stream 4 with autoDestroy: false) was never released, unlike a Node.js Readable. It is now released under its guard once the send has failed, destroyed without its error (which it has emitted already), or paused without destroy(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The response was the only caller that passed emitted as true, to leave an error that the source emitted to its guard. Now that the error listener of a send counts as hearing its source (hear()), the guard never reports an error that a send hears, so that branch is dead and the send reports such an error itself, once. reportLate(source, error, report) now reduces to the report of a late error once. The two call lines of the response (the stop path of a send, and the send of a source that cannot be read) drop the argument, and the unit test that only kept the dead branch covered is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…21 in binden The send() paragraph tells which streams keep their guard for life, that a source without an error listener is left untouched, that the listener of error comes first, how errors emitted while a guard is installed or heard by a send are reported, the messages of stream-like release failures, the errored and falsy cases of stream-like sources, chunks that are not bytes, and empty bodies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… of binden The README described a stream that refuses the listener of close of its guard like one that refuses its listener of error. They differ: a refused error listener keeps the listener of error of the send and destroys the source without the deciding error, while a refused close listener is logged once and leaves the listener of error of the guard in place for the life of the source (the guard no longer hears its close), the send's listener removed, and the source is still destroyed with the deciding error (R3's close-refusal2.mjs node|like error|close on 6b3bf40). The refusal record is now stated once for both. MIGRATION-4.md said that the send of binden 2.0.4 never settled when the client went away first, which is false for a stream that fails by itself: the echo of a request whose client goes away rejected with ECONNRESET in 2.0.4 (mig-echo-gone.mjs), while an ordinary stream stayed pending (mig-gone.mjs 64). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…line to race in binden stream-like-destroyed.ts destroyed /ended on a 2 ms timer under the 50 ms deadline of the application, and its premature close is decided on the turn after that destroy. A stall of the process once the timer is scheduled lets the deadline pass first, which replies 503 instead of 500 (the macOS flake of round 21; R3's stall2.mjs with STALL=60: "503 204 500 500 500 503"). Every request but /cancelled, the only one that waits for its deadline, now removes it (context.setTimeout(0)); the others are decided at once, or on the turn after the destroy of /ended. That destroy stays on a timer rather than a setImmediate(), as a0c71d6 triggers its sources: its premature close (a setImmediate() of the send) then follows in the same turn of the event loop, before the timer of the failure of its destroy 20 ms later. From a setImmediate(), a stall of 25 ms or more right after the destroy let that failure come first, and the send rejected with it (the fixture timed out waiting for its late error). A 60 ms stall right after the trigger is scheduled, inside it once it has destroyed the source, or at the start of each middleware but that of /cancelled, gives the expected output on Node 26.11.1, 24.16.0 and 24.14.1, where the old fixture printed 503 for /ended on the first and the last. Under CPU burners the test passed 40/40 on Linux (node:26.11.1, also with two burners per CPU, and node:24.14.1) and 40/40 on macOS (26.11.1 under burners, 24.16.0). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
V8 formats the stack of an error once it is first read, and until then its frames hold their objects, such as the receiver of each method (the exchange or the response). A source that keeps the error it is destroyed with (minipass keeps its last emitted error) or an application that keeps the error its send rejects with then retained the request and the response for as long as it lived. The reason of an aborted signal (a BindenAbortError), the premature close, the length mismatch and the refusal of a pipe now have their stacks formatted as binden creates them, as Node.js does for an error it destroys a stream with. A prepareStackTrace of the application that throws leaves the stack to it. The argument errors of a call (a TypeError or a RangeError) are thrown or rejected to its caller, as by any Node.js API, and are not changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ue in binden When code of a Readable throws a value whose stack getter throws, the send rejects with it and destroys the source with it, but Node.js reads the stack of the error a stream is destroyed with, so the destroy threw before anything was destroyed: the release was logged and the source (e.g. a file stream) stayed open with its descriptor. The release now falls back, as the stream-like adapter does, to an Error that carries the thrown value as its cause, whose stack is formatted and which is recorded as decided, so that the guard of the source does not log it once the source emits it. A destroy that throws once the source is destroyed, or one without an error (of a send that has resolved), is logged as before and not called again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in binden The pending decision of a stream-like send was one value per source: a send of the same source that its code starts while another one decides (e.g. from its own destroy) deleted it on return, and the error that the source then emitted was reported as a late error, although the outer send still rejects with it (Codex, round 21; R2-1). decide() now runs a call while a send decides an error, counted per source and error, so that an inner decision with the same error that ends first leaves the outer one pending. Only an object is known as decided: a primitive that decided a stream-like send and that its code throws again for the listener of its guard was not logged, unlike for a Node.js Readable, while the README says that a primitive is logged (R3.21-2). isKnown() is the one check of the guard, of the other reports, and of the refusals. This also adds the interfaces that the response needs from the adapter. decidedFor() records an error that a source has emitted and that has decided its send for that source only (the record of the reports), so that the same object of the application can still be a late error of another source (R1 F2); a refusal of the guard of that source with it is not logged either. resolved() tells a readable of a stream-like source that its send has resolved, so that it destroys its source without an error, never with the reason of a cancellation (R2 R21-H3). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…binden A Readable destroyed with an error before its send rejects at once with that error, but the error was decided only after its guard was installed: code of the source that emitted it meanwhile (a listener of its newListener) had it logged as a late error, although the send rejects with it. The pending decision is now in place around the installation of the guard (decide() of the adapter), which replaces the local filter of the refusals of the guard. An error that a source emits and that decides its send was recorded as decided for every source, so that one object of the application that failed one send (e.g. a shutdown error that it destroys each of its streams with) was never logged as a late error of another source. It is now recorded for its source only (decidedFor()), as is the error that a destroyed Readable rejects with. The global record stays for the errors that binden creates itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… has heard it in binden A send whose response another writer has ended reported an error that its source emitted at once, as a late error. One source sent to such a response and to a live one (e.g. a shared PassThrough) then had the error logged as late when the stopped send listened first, although the live send rejected with it in the same event: the order of the listeners decided the log. A stopped send now reports the error of its source on a later turn, once the event that may have brought it has reached every listener, so that a live send of the source has decided it first, and it is not logged in either order. The close of a source destroyed with an error that its _destroy() swallows brings its errored to every listener as well: it is handled as an emitted error, decided for the source by the live send and reported later by the stopped one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lves in binden A Node.js Readable is destroyed without an error when its send resolves, but a stream-like source was destroyed with the reason of a cancellation: a premature close once another writer had ended the response (e.g. the 406 of a compression), or the abort of the client once a complete declared length had been sent and the client closed. Its own error listeners and its errored then reported a failure for a successful response. The send now tells the adapter (resolved()) before it releases its source when it resolves, and when a chunk comes once another writer has ended the response, so that the source is destroyed without an error, as a Readable is. Native pipeline() errors such a source: binden keeps parity with its own Readable path instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lled in binden The running-error path installed the guard of the source through settle() before the error that fails the send was decided, and the destroyed path installed it before it read and marked the errored of the source. Code of the source that threw that error again for a listener of the guard (e.g. a listener of its newListener) was logged as a refusal, and one that emitted it then was reported as a late error, while the send rejected with it (Codex R21-C2, R1 F1). The same held for an error that the readable is destroyed with (e.g. what a pause() of the source has thrown), whose record came after the guard. Each path now runs its guard installation inside decide(): fail() decides the error of the source around settle(), the destroyed path reads the errored first and installs the guard inside the decision (also when that read throws, so that the guard still listens), and the destroy hook of the readable decides the error that it is destroyed with around the whole release. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd for its source in binden The destroy hook of the readable of a stream-like source recorded every error that it destroys the source with as decided for every source: one error object of the application (e.g. what a pause() of the source has thrown, also used to destroy other streams) was then never reported as a late error of another source, the same defect as R1 F2 for an error emitted by a Node.js Readable. An error that the readable is destroyed with is now recorded for its source only (see decidedFor()); only the reason of a cancellation, an error of binden, stays recorded for every source (see decided()). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…royed with in binden The adapter destroyed a stream-like source with the reason of a cancellation (the reason of the signal of the request, or a premature close) whose stack V8 had not formatted yet. Such a stack keeps the objects of its frames, so that a source that keeps the error it is destroyed with (e.g. minipass 7, which keeps the error that it emits), and that the application keeps, retained the request and the response for its life (R2 R21-H1: 60 of 60 alive after a collection). The adapter now reads the stack of that reason once before the source has it (a stack that cannot be read is handed over as it is). The errors that the readable is destroyed with are formatted by Node.js already, which reads their stack. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…binden
A chunk of a stream-like source that the push() of its readable refuses by throwing (e.g.
a view of an ArrayBuffer that has been detached, which Node.js cannot wrap in a Buffer)
threw into the emit("data") of the source, out of code of the application, while the
send went on (R2 notes).
The push() is now protected as the pause() that follows it: what it throws fails the send
through destroyWith(), as for any other failure of the source, which is destroyed with it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e in binden The logs of the adapter call the logger of the application. One that threw inside the destroy hook of the readable (e.g. for a removal of a listener, a destroy of the source, a refusal of its guard, or a source left untouched) escaped from it: Node.js then destroyed the readable with the throw of the logger instead of the error that decides the send, the other listeners of the send stayed on the source, and the source was neither destroyed nor paused (R2 notes). The deferred removal of the listener of error, on a turn of its own, crashed the process when its log threw. Every log of readStreamLike(), and the notice of a source left untouched, is now quiet: a log that throws is ignored, and the release goes on, as binden already does for a logger whose child() throws. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in binden
A guard that leaves on the turn after the close of its source removed both of its
listeners in one try, and reported a removal that threw as a late error of the source
("A released stream has failed"), while every other removal is logged as "The removal of
a listener of a stream has failed"; after the first throw, its listener of close was left
on the source (R3.21-6, for stream-like sources and Node.js Readables alike).
Each of its listeners is now removed on its own, and a removal that throws is logged with
the message of a removal by the report of the source, which holds only the logger: the
late report of a request now takes a message of its own (IOwnership.released). A logger
that throws then no longer escapes from the turn of the guard.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…treamLike() in binden The JSDoc of readStreamLike() still said that a removal of a listener that throws is reported (see ISourceReport), while it is logged by its logs (see IStreamLikeLogs) since round 21 (R3.21-5). It now says so, and tells that the reason that a cancelled source is destroyed with has its stack formatted, and that a source whose send has resolved is destroyed without an error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…or its source in binden Three follow-ups of round 22 (items 2 and 3). The adapter formatted the reason that it destroys a stream-like source with by a helper of its own: it now uses formatStack() of the exchange, the one helper of binden (no import cycle: the exchange imports neither the adapter nor the response). The other errors of the adapter that a source or a send gets (a chunk that is not bytes, a premature close) are formatted by the destroy() of Node.js, which reads their stack before anything has them: the tests now check it. The framework created the 503 of the deadline, which the error handler is given, and the 500 of an error handler that has not settled in time, which its pending sends reject with, without formatting their stacks: an application that kept one retained the frames of its creation (R2 R21-H1). Both are now formatted. A value that code of a Node.js Readable throws (its own resume(), at once or once the response drains, its pause(), its on(), or a listener of its newListener) was recorded as decided for every source, although it is an object of the application, which may still be a late error of another source (as R1 F2 for an emitted error). It is now decided for that source only (decidedFor()); an error of binden or of Node.js (e.g. a body longer than its Content-Length) stays decided for every source. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eleases of round 22 in binden The Lifecycle section tells that the errors binden hands over have their stacks formatted, and the send() paragraph tells when an error is decided before the guard and for which stream, that a stopped send logs on a later turn, that a resolved stream-like source is destroyed without an error, the fallback destroy of a Readable, unpushable chunks, removals of a guard and throwing loggers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
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.
Checklist:
npm run ci:alllocally and all tests are passing (Node.js 24.16 and 26.11).Description
Moves
binden,@binden/logger,@binden/body,@binden/compressionand@binden/multipartinto this repository as npm workspaces (packages/*). All packages are released together with one version by semantic-release. Besides the infrastructure and CI, it updates the dependencies, fixes the library bugs found while reviewing this PR (see Library fixes and Breaking changes), and relicenses every package under MIT.What changes
History.
bindenis moved withgit mv, so its history and tags are kept. The sources oflogger,body,compressionandmultipartare copied without their history: it stays in their repositories, which will be archived. The import commit lists the upstream SHAs. The unreleased changes of@binden/logger(pino 10, MIT license) are separate commits, so they show up in the release notes.Lockstep releases.
scripts/release.mjsis called from@semantic-release/exec:verify: fails if any package is already published at the next version;version: sets the version in every workspace, rewrites internal dependency ranges to^<version>and refreshes the lockfile (in the checkout of the release job only);publish: publishes the packages in dependency order (logger → binden → body → compression → multipart).npm run release:check(run in CI) checks that all workspaces stay in lockstep. Only a confirmedE404from the registry counts as "not published" inverify(network or auth errors stop the release before anything is tagged), andpublishruns the lockstep check first.Recovery. semantic-release pushes the tag before the packages are published. A release that fails after its tag is recovered by running the CI workflow by hand (
workflow_dispatchwith the version): therecoverjob checks out the tag, publishes the packages that are not published yet (with provenance) and creates the missing GitHub Release.publish --skip-existingrefuses to run from another commit than the tag, and refuses a package already published from another commit (itsgitHead). Tested with a fake npm (failure after each workspace, recovery, wrong commit).No release commit. A release creates the
v<version>tag, publishes the packages and creates a GitHub Release whose notes are the changelog. Nothing is committed back tomain: a release commit pushed withGITHUB_TOKENwould be rejected by the required status checks, and semantic-release recommends against it. The versions in the repository therefore stay at0.0.0-development(internal ranges^0.0.0-development). The rootCHANGELOG.mdpoints to the GitHub Releases and to the changelogs of the former packages.@semantic-release/git,@semantic-release/changelogand the GPG setup of the release job are removed.Configs live in the repo. They are written in place instead of coming from the
@binden/*-configpackages:tsconfig.base.jsonplus a one-linetsconfig.jsonper package;buildtype-checks the tests as well.@binden/eslint-config@7.0.1and@binden/eslint-config-ts@6.0.2. Entries that change nothing were dropped; effective rules were verified to be identical. Rule violations in code carried over from the old repositories are switched off per file, with a comment.Tests.
node:testwith tsx. Every workspace runsscripts/test.mjs, which passes extra options to Node.js before the test files (npm test -w packages/body -- --test-name-pattern=JSON,npm test -- --test-reporter=dot).@binden/loggerhad no tests (and was missing from the coverage report, sincebindenloads it fromdist):node:testtests are added.all) and enforces 100% lines/branches/functions/statements, so an untested package or line fails CI.listen(0)), and the files of theBindenResponsetests live in a new temporary directory per test, so a local service on a fixed port or a second run cannot break the tests.packages/*/test/setup.tsdisables HTTP keep-alive for the test clients (node:httpagent, undici global dispatcher, also used byfetch): some tests depend on the client closing its connection after the response, and a pooled socket would outlive its test.@binden/multipartonly gets type annotations required by the current@types/nodeand@types/busboy(Buffer<ArrayBuffer>,filename as string).Tooling works on Windows:
prebuildremovesdistwithscripts/clean.mjs(norm -rf), npm is run throughscripts/npm.mjs(Node.js withnpm_execpath, sinceexecFile("npm")cannot runnpm.cmd), the tooling tests use no shebangs,chmod,:-joinedPATHor symbolic links (a junction), and.gitattributeschecks out text files with LF.CI: one workflow,
ci.yml:lint-stagedonly (npx --no).linton Node 24: lockstep check, Prettier, commitlint for the pushed range, ESLint, tooling tests (scripts/*.test.mjs: release script with a fake npm, npm launcher, test runner, pack check), the packed files of every package againstscripts/pack-files.json, consumer check, a release rehearsal (semantic-release in dry-run mode with the real configuration in a clone with a local mirror, the next version and its notes, thenrelease.mjs verifyandversion), TypeDoc (treatWarningsAsErrors).scripts/consumer-check.mjs): the packed tarballs are installed into a fresh project with--install-strategy=nested(so an undeclared dependency fails, as it did forpinoin binden), type checked and run as ESM against a real server. Everytypescriptblock of the package READMEs (62 examples) is type checked (not run) against the same installation, with the samestrictoptions. Three combinations: TypeScript 6 with@types/node24 (skipLibCheck: false), TypeScript 6 with@types/node26 (skipLibCheck: truebecause of an upstreamthread-streamissue, see Known issues in the README), and TypeScript 7 with@types/node24.teston Node 24.14.1 (the oldest supported version), 24 and 26 with coverage; Coveralls upload from Node 24.windows(new):npm ci, the tests and the tooling tests onwindows-latestwith Node 24.releaseonmain, afterlint,testandwindows: semantic-release (tag, npm, GitHub Release), without a release commit (see above).recover, by hand only (see Recovery).pagesonmain: deploys the TypeDoc site to GitHub Pages after the checks, without waiting for the release (a failed release no longer skips the docs).Engines. The packages (and the tooling) require Node.js
>=24.14.1and npm>=11.11.0; the packages are compiled for ES2024..envfiles are ignored again; theinstall:cleanscript (which deleted the lockfile) is removed.Dependencies
Updated to their latest versions: pino 10.4.0 (
@binden/logger),@types/busboy1.5.5 (peer of@binden/multipart), and for the tests fast-json-stringify 7.1.0, sinon 22.1.0 (with@types/sinon22) and undici 8.11.2 (@binden/body); the lockfile is refreshed within the declared ranges. binden no longer importspinoat all (its serializers are built from an allowlist).Deliberately not updated (explained in the README, Dependencies):
<6.1.0) and TypeDoc (≤6.0.x) need the JavaScript API that the native compiler does not have yet. The repository builds with TypeScript 6; the consumer check type checks the packages and the README examples with TypeScript 7.@types/node26: the types follow the oldest supported Node.js (24); the tests run on 24 and 26.conventional-changelog-conventionalcommits10: semantic-release 25 usesconventional-changelog-writer8, and preset 10 requires writer 9 (a release dry-run fails with it).npm audit --omit=devreports no vulnerabilities. The 13 advisories of the dev dependencies are in the release tooling and have no fix yet:braces3.0.3 (throughmicromatchof semantic-release) is the latest version, and the others are bundled with npm 11.21.0, the latest npm that@semantic-release/npmsupports.Library fixes
Inherited from the former repositories (or found by the reviews of this PR), each with a regression test checked by reverting the fix:
Responses (
binden)send()settles on what reaches the connection: a body (string, buffer orReadable) is written with a callback, so that a failed write rejects (Node.js 24 emitsfinishafter a failed write once the response is ended); aReadablecounts a chunk once it is flushed. It rejects when aReadablefails, closes before its end, or does not have itsContent-Length: nothing sent yet, the error can still be replied; otherwise the response is cut, so that the client knows that it is truncated.Content-Length(a stale one is replaced), 1xx/204/304 have none, 205 hasContent-Length: 0,HEADgets the length of the body it would send. Before, a staleContent-Lengthleft clients waiting or desynchronized keep-alive connections (204, 205, the 416 ofsendFile(), error replies).Readablethat is not sent (toHEAD, 1xx/204/205/304, or an ended response) is destroyed with its errors ignored: a failingdestroy()crashed the process, and an ended response kept the file open.set()sets the own properties only (an inherited, or polluted, property was sent as a header).sendFile()reads the metadata and the content from one file handle (no more than its size is sent; a FIFO is not waited for), and evaluates the preconditions in RFC 9110 order:If-Match/If-Unmodified-Since→ 412,If-None-Match(weak comparison) → 304 or 412,If-Modified-Since→ 304, thenRange(GET only, satisfiable ranges merged, 416 otherwise) withIf-Range(a strongETag, or theLast-Modifiedwhen it is a strong validator).Requests and routing (
binden)request.URLis assembled from the protocol of the connection, the authority (of an absolute-form target, otherwiseHost) and the path of the request-target://evil.test/xis a path,https://…targets no longer makesecuretrue, and dot-segments, fragments, backslashes, invalid percent-encoding and userinfo get 400.request.secureisprotocol === "https:".trust_proxy(defaultfalse) decides whetherForwardedis trusted at all; the trusted element is taken by its position among the raw elements (an invalid element of a proxy no longer lets the one of the client take its place), with fail-closed parsing.request.querykeeps its prototype (__proto__keys), cookies are validated (no attribute injection) and an empty cookie value is kept, and the header parsers follow RFC 9110:Authorization,Content-Type(every parameter kept and quoted),Content-Encoding(unknown codings → 415 in@binden/body),Range,Content-Range,Accept-Encoding(qvalues),If-Modified-Since(HTTP-date only); their constructors reject what the parsers reject.HEADhandler is used, RegExp paths keep their flags (minusg/y), a guarded router that refuses the method passes the request on (405 with the union ofAllowwhen nothing handles it),CONNECTroutes are rejected (Node.js never routes them to the middlewares).Errors and logging (
binden)donewithout a reply (an empty response), the context of another request returned by a middleware (500), any thrown value (null, a symbolstatus, a throwing getter → 500), an asyncerror_handler(awaited; nothing started → the default reply; a reply that is not ended once it settles is cut by closing the connection). A handler that never settles keeps its request: there is no deadline.send(): with the queued cookies, the framing above, and without the representation headers of the failed body. Status codes are any integer from 100 to 599.child()throws is used itself, logger calls are guarded, and the serializers never throw (a cyclic chain of prototypes is bounded). The default logs are an allowlist: a request is its method, its path (without query, fragment and userinfo; the path itself is logged), and a few headers; errors are their built-in type,status,exposeand a Node.js errorcode.Middlewares
@binden/compression: completes like an uncompressed response (end()callback,writableEnded, backpressure, errors), never compresses partial responses or responses without a body, negotiates by effective quality, and weakens a strongETagof what it compresses.@binden/body: a newlimitoption (decoded bytes, 413, reading stops; none by default, as decided, the README recommends one), decoding with thecharset, 415 for an unknown coding or charset, andundefinedfrom a customparse(as Ajv parsers return) is a failure (415). A client disconnecting during the upload no longer leaves it pending.@binden/multipart: a newlimitoption for the whole request (413, reading stops; none by default, the README recommends limits for the files, which are held in memory), a cut-off file part no longer crashes the process,throw_limitsrejects truncated fields, and an aborted upload settles.READMEs: the examples compile (type checked with TypeScript 6 and 7), the Router, the request-target and the contracts above are documented, and
SECURITY.mdasks for private reports.Breaking changes (besides the move)
trust_proxy:request.protocol/secure/URLignoreForwardedunlesstrust_proxytrusts the proxies (an application behind a TLS-terminating proxy needsnew Binden({ trust_proxy: 1 })).secureand the protocol ofURLno longer follow the scheme of an absolute-form target.new Cookie()throws aTypeErrorfor an invalid name, value or attribute;Cookie.fromString()skips invalid cookies.ContentEncoding.fromString()/request.content_encodingreturnnullfor an unknown coding; the constructors ofRange,AcceptEncodingandContentTypethrow aTypeErrorfor invalid input, andContentType#toString()includes every parameter.response.status()andnew BindenError()throw aTypeErrorfor a non-integer status (such as"400"); any integer from 100 to 599 is accepted.router.on("CONNECT")/off("CONNECT")throw aTypeError.sendFile()takes the path only (theStatsargument is removed).@binden/body: a customparsethat returnsundefinedmakes it reply 415.@binden/body,@binden/compressionand@binden/multipartare now MIT (likebindenand@binden/logger).First release: 4.0.0
@binden/compression@3.0.0is already on npm, so the first common version must be higher. The annotated tagv3.0.0(already pushed) is a bootstrap marker on the binden 2.0.4 release commit (the same commit asv2.0.4). Counting from that tag, a semantic-release dry-run (now also run by CI, see release rehearsal) gives 4.0.0, with the breaking changes above in its notes. The release notes contain only the unreleased changes (ct_text→text/plain, MIT license for binden and logger, pino 10), the custom logger feature, the library fixes above and the migration.The tag was moved once (from an earlier baseline commit) and will stay where it is now. A clone that fetched the old tag reports
would clobber existing tagongit fetch --tags; fix it withgit fetch --tags --force(orgit tag -d v3.0.0 && git fetch --tags).Old per-package changelogs stay in
packages/*/CHANGELOG.md; the notes of the new releases are the GitHub Releases.Before merging
main:/docs, which this PR removes).binden-js/binden, workflowci.yml) for all 5 packages, or provide anNPM_TOKENthat can publish all of them.logger,body,compressionandmultipart, so they cannot publish out-of-band versions.main: required status checkslint,test (24.14.1),test (24),test (26)andwindows(none are required now), a required review, andenforce_admins. This is compatible with the release now that it does not commit tomain.releaseenvironment with required reviewers for therelease/recoverjobs; private vulnerability reporting (seeSECURITY.md), secret scanning and push protection.GPG_KEY,GPG_KEY_ID,GPG_KEY_PASSPHRASE,GIT_COMMITTER_EMAIL,GIT_COMMITTER_NAME.🤖 Generated with Claude Code