Commit Graph
22360 Commits
Author SHA1 Message Date
Kamil Myśliwiec 3acb8e574a chore(release): publish v12.3.0 release v12.3.0 2026-10-27 10:00:20 +01:00
Kamil MysliwiecandRoman Khegay 5f4075074f feat(node): add a native node http platform adapter (#18063)
* feat(node): add a native node http platform adapter

Add @nestjs/platform-node, an HTTP adapter built directly on the
node:http and node:https modules. Routes use the same path syntax as
with the Express adapter and are matched through a find-my-way radix
tree; middleware runs in an Express-compatible stack. Body parsing,
CORS, static assets and template rendering are built in.

* test(node): run the shared integration suites against the adapter

* chore: add node and nest-node targets to the benchmarks

* feat(node): limit route parameters to 100 characters

As with Fastify, a route no longer matches when one of its parameters
is longer than 100 characters (decoded): the request falls through to
the next matching route, or gets a 404. Wildcards are not limited.

* feat(node): add the multer file upload interceptors

Ship FileInterceptor, FilesInterceptor, FileFieldsInterceptor,
AnyFilesInterceptor, NoFilesInterceptor and MulterModule with the
package, as @nestjs/platform-express does, instead of borrowing them
from it.

* docs(node): add the package readme

* test(node): map @nestjs/platform-node in the integration tsconfigs

* fix(node): match routes that differ only by parameter names

Routes of the same shape registered with different parameter names
(GET /users/:id for one version, GET /users/:userId for another) now
share one route and pass control to each other through next(), as with
Express, and each handler sees the parameter names of its own route.
Text right after a parameter ("/users/:id@profile") no longer ends up
in the parameter's name.

* test(node): listen on 127.0.0.1 in the node adapter specs

Use listenOnLoopback() in the Node adapter's own supertest specs, as

* fix(node): fix edge cases in responses, routing and body parsing

- next(null), next(false) and other falsy values are no errors, as
  with Express, so that "cb(err => next(err))" works
- req.path is the path the router matches (decoded, absolute-form
  targets resolved), so that middleware checking it sees that path
- an empty 204, 205 or 304 reply drops its Content-Type, as does
  sendStatus() for those statuses; a StreamableFile error response
  sends its own Content-Length rather than the file's
- res.type() looks up extensions as Express does, send() and json()
  send a Content-Length in answer to HEAD requests, and a Referrer
  header wins over Referer
- body parsers call next() when the client is already gone, report a
  mid-body abort as "request.aborted", validate parameterLimit and
  depth, take a null limit for the default, keep the status of errors
  thrown by verify, reject whitespace-only JSON, drop a UTF-8 BOM, and
  reject "__proto__" keys in urlencoded bodies with 'error'

* test(node): add unit tests

Cover the adapter, the request and response helpers, the router and
its route tables, the body parsers and the view renderer.

* chore: write the raw node benchmark headers in a single call

* fix(node): decode windows-1252 bodies the same on every node version

TextDecoder in some Node.js 22 and 24 releases (22.15, 24.4 among them) decodes windows-1252, and labels mapped to it such as cp1252, as ISO-8859-1, so bytes 0x80-0x9F came out as C1 controls instead of characters like the euro sign. Map those bytes ourselves.

* feat(node): add res.redirect(), res.locals and req.is()

Express-compatible helpers that common middleware relies on (passport and OAuth flows redirect, middleware passes data along in res.locals, some check the request type with req.is()). res.redirect() encodes the Location, negotiates a plain text or HTML note from Accept and adds Accept to Vary, matching Express 5.

* fix(node): match routes with the method set by earlier middleware

The router read req.method once per request; Express reads it for every layer, so middleware such as method-override could not change which route runs. Read it per layer.

* feat(node): add res.append(), res.cookie() and req.app for middleware

Popular Express middleware calls these: express-rate-limit sets its headers with res.append() and reads req.app.get('trust proxy'), csrf-csrf sets its cookie with res.cookie(). res.clearCookie() comes along. They follow Express 5, using the same cookie and cookie-signature packages; req.app only reports the adapter's fixed settings ('trust proxy' is false).

* fix(node): match wildcard routes and fall through to other routes

- a route such as "/admin/{*path}" or "/admin/{:id}" no longer matches "/admin" (find-my-way's ignoreTrailingSlash matched its "/admin/" variant there), so it matches the same paths as its middleware, as with Express
- once every handler of the most specific route calls next(), the other routes matching the path run, each with its own parameters, so a version- or host-filtered handler that does not apply falls through to ":id" as with Express and Fastify
- a wildcard is split before it is decoded, keeping an encoded "/" within its segment

* fix(node): pass errors from middleware on to exception filters as is

mapException() turned http-errors-style errors into a new HttpException, so a @Catch(MyError) filter and anything reading the error's code (EBADCSRFTOKEN, ...) no longer saw them. Return them unchanged, as the Express adapter does; Nest's exception filter answers them with their status and message as before.

* fix(node): pass the views directory to view engines such as hbs

res.render() in Express passes its settings, including "views" and "view engine", to the engine's __express(); hbs reads "views" to find layouts, so @Render() with hbs failed with a 500. Pass them the same way.

* fix(node): align the body parsers with body-parser 2

- read the charset with content-type 2, as body-parser does, so a Content-Type with a trailing ";" keeps its charset
- accept limit: Infinity and the tb/pb units (NaN and negative limits still throw)
- add the urlencoded defaultCharset, charsetSentinel and interpretNumericEntities options
- pick the byte order of "utf-16" bodies from the byte order mark, or else the content, as iconv-lite does

* fix(node): send binary, redirects and 404s as other adapters do

- send typed arrays and DataViews as binary, as Buffers already were (as Fastify does)
- @Redirect() sends the note and "Vary: Accept" res.redirect() sends, as with Express
- res.send() sends numbers and booleans as JSON, as Express 5 does
- res.json() sets no Content-Type when serializing the body throws
- the 404 for a request outside the global prefix echoes the path as received rather than decoded, with "X-Content-Type-Options: nosniff" and "Content-Security-Policy: default-src 'none'", as Express' final handler does

* test(node): expect the bare path to miss an optional wildcard route

As with Express, "/v1/users/" reaches the converted "{*path}" catch-all but "/v1/users" does not; the spec had encoded the old Node adapter behavior.

* fix(node): match every path case-sensitively

find-my-way routes were case-sensitive while the routes matched with path-to-regexp (wildcards, optional groups) and the middleware were not, so "GET /CATS" ran the middleware of "cats" and then got a 404, and "/ADMIN/x" reached the "admin" catch-all. Match all of them case-sensitively, as find-my-way does by default (and Fastify); the case-insensitive alternative would lowercase every request path.

* feat(node): make the parameter limit configurable and answer 414

new NodeAdapter({ maxParamLength }) sets the longest route parameter a route matches (100 by default, as with Fastify). A request whose parameter is longer still falls through to another matching route, such as a sibling wildcard; when none matches, it now gets a 414 (URI Too Long) rather than a 404, as with Fastify. The 414 goes through Nest's exception handling like other errors, so filters can catch it.

* test: compare the http adapters on routing, errors, views and parsing

A new integration suite runs the same applications and requests on the Express, Fastify and Node adapters, where they support the feature, with the same expectations:
- wildcard and optional-group routes, and which requests their middleware sees
- falling through to less specific routes from version filters (header, media type, custom, neutral, several versions, all methods, global prefix, URI) and host filters
- errors raised by middleware, as @Catch() filters and Nest's exception handling see them
- @Render() with an engine that finds partials through the views directory
- @Redirect(), res.send() and binary bodies
- body parser charsets, limits and urlencoded charset options
- Node specifics: the route parameter limit and its 414, case-sensitive paths, and the final 404 outside the global prefix

* feat(node): let responses send files as express does

Adds Express' res.sendFile() on the same send package: Content-Type, Content-Length, Last-Modified, ETag, ranges and 304s, the root, headers, maxAge, dotfiles and etag options, and a callback for the outcome. Without a callback, a failure goes to the error-handling middleware and Nest's exception filters, as Express does through next(): a missing file is a 404 and a directory answers like an unmatched route. The adapter reaches its router from the request's server for that, so nothing is stored per request.

@nestjs/serve-static calls it for its index.html fallback. send and on-finished were already loaded through serve-static; an interleaved wrk comparison (4 rounds), the router microbenchmark and the import time show no difference.

Also sends the JSON error body of a reply whose Content-Type was not JSON as application/json; charset=utf-8, as Express ends up doing.

* test(node): cover the shutdown 503 while the hooks run

The adapter already answered 503 with Connection: close once beforeClose() ran, from the first layer of the router, before Nest middleware, and destroyed connections only in close(). Cover what #18101 covers for Fastify's return503OnClosing: a request during a slow onModuleDestroy() gets the 503 without reaching middleware, requests keep being served without the option, and forceCloseConnections leaves kept-alive sockets open during the hooks.

The 503 now says Content-Type: text/plain; charset=utf-8, like Fastify's and the adapter's final handler.

* fix(node): match excluded routes like the router does

Report through HttpServer.getRouteMatchingOptions() that the router
matches paths case-sensitively and ignores a trailing slash, so that
MiddlewareConsumer.exclude() compiles the excluded routes the same way.

* fix(node): expose package.json in the exports map

As the other packages do since #18122.

* chore(node): align the package version with the 12.1.3 release

The v12.1.3 release bumped the exact @nestjs/common and @nestjs/core
devDependencies of the other packages. Left at 12.1.2 here, they no
longer matched the workspace once master was merged in, so npm
installed both from the registry under packages/platform-node and the
adapter ran against its own copies: its HttpException, StreamableFile
and VERSION_NEUTRAL were not the ones the core checks for.

* Update packages/platform-node/body-parser/body-parser.ts

Co-authored-by: Roman Khegay <rk@garuna.dev>

* perf(node): parse a repeated content-type charset once

Clients send the same Content-Type with every request, so the charset of the last header parsed is kept instead of parsing the header again. Media types without parameters still return before it.

Co-authored-by: Roman Khegay <rk@garuna.dev>

* test(node): cover a content-type that changes between requests

The same parser reads Latin-1, UTF-8, no charset and Latin-1 again, so a charset cache that misses a change fails.

* Update packages/platform-node/body-parser/body-parser.ts

Co-authored-by: Roman Khegay <rk@garuna.dev>

* Revert "Update packages/platform-node/body-parser/body-parser.ts"

This reverts commit 88e446db1b, which applied the charset cache suggestion a second time on top of 13619b662e, declaring the cache twice and leaving a partial copy of getCharset().

* chore(node): align the package version with the 12.2.0 release

As every sibling did in the release, so that the workspace links the
adapter to this checkout's @nestjs/common and @nestjs/core.

* fix(node): answer request targets that are not a path with 404

Node.js accepts request targets such as "*admin/users", which
find-my-way routes as "/admin/users", while no middleware path matches
them, so the route ran without its middleware. "OPTIONS *" got the
"OPTIONS /" route. RouteTable.lookup() now matches no path that does not
start with "/", so these get the not-found handler, as on Express and
Fastify (a03c7a1db).

Co-authored-by: Roman Khegay <rk@garuna.dev>

* fix(node): send the standard body for an exception wrapping an error

JSON.stringify() sends the enumerable properties of an Error, e.g. the
request config of an HTTP client error, so an Error body with an error
status now gets the standard body of the status, as on Express
(876a182c6). An object error body without a "statusCode" of its own also
goes out as JSON when the route set another Content-Type (7dc6e349d).

Co-authored-by: Roman Khegay <rk@garuna.dev>

* fix(node): handle a failed streamable file and one for a head request

A source that failed before the reply (e.g. while an interceptor
awaited) has already emitted its "error" event, so the request hung until
the client gave up; it now goes to the error handler of the file, as on
Express and Fastify (1933cc9ed). A HEAD response no longer pipes the whole
source, which Node discards without applying backpressure; it ends once
the source is readable (8b8e8396f). The specs expect the "Bad Request" the
default error handler now sends (2f28d91af).

Co-authored-by: Roman Khegay <rk@garuna.dev>

* fix(node): run wildcard middleware on "/api/" for a named wildcard

The exact "/api" entry skipped "/api/" so that the middleware would not
run twice next to "/api/{*path}", which left "/api/*path" (one more
segment needed) running it 0 times there. The core now runs a middleware
for the first of its paths that matches a request only (fb8c2cac7), so
the entry is registered as is, as on Express (b75244ffd).

Co-authored-by: Roman Khegay <rk@garuna.dev>

* fix(node): parse quoted media type version parameters

The copy of getMediaTypeVersion() is master's (c8923bd7c): quoted
values are unescaped, and "," or ";" inside quotes no longer split the
header, so `Accept: application/json;v="2"` matches version 2, as on
Express and Fastify.

Co-authored-by: Roman Khegay <rk@garuna.dev>

* fix(node): copy the multer fixes of platform-express

Copied again from platform-express: the limit fields of the options
(#18074), the default fieldNestingDepth and fieldArrayIndexLimit
(94fcc9377), removing the uploads of a request a pipe rejected
(acb98de89), and a route's dest replacing the module's storage
(e075cda3c). The parity spec checks the upload directory of Node too.

Co-authored-by: Roman Khegay <rk@garuna.dev>

* fix(node): set the security headers on router error responses

The router answers a request target it cannot decode (a malformed URL,
400) without running any middleware, so the response had none of the
headers set by useSecurityHeaders(). NodeRouter now reports such a
request to onRejectedTarget, where the adapter runs the security hook
for its headers, as the FastifyAdapter does for the errors its router
answers (c04a9182b).

* test(node): expect the sse error event and csrf answers of master

The SSE error event carries "Internal server error" (97ab04494). Node
serves "/api/hooks/github/" with the "github" route, as Express does, so
an exclusion of "hooks/github/*" still leaves it to CSRF protection: a 403
(7b01d0e7a9).

* fix(node): write the html redirect note as express 5.3 does

Express 5.3 (in master's lockfile) sends the HTML note of res.redirect()
with a doctype, a title and a body. The Node response does the same, and
the spec shared by both expects it.

* test(node): wrap the source in a streamable file before destroying it

The source emitted 'error' before StreamableFile attached its listener,
so Vitest reported an uncaught exception and the unit test jobs exited
with code 1 although every test passed. The Express spec on master
creates the file first as well.

* fix(node): map 4xx http-errors errors to http exceptions

As master now does for Express (#18214, #18232) and Fastify (#18239): an
http-errors-style client error, such as a 413 from a body parser or a 403
a middleware passes to next(), reaches exception filters as an
HttpException with its status and the original error as its cause, so
@Catch(HttpException) filters run for it. A 5xx one stays unmapped, so the
built-in exception filter still logs it. The shared middleware errors spec
expects the new behavior on all three adapters.

Co-authored-by: Roman Khegay <rk@garuna.dev>

* fix(node): end a head response for an empty streamable file

A source that has no data, or has already ended, never emits 'readable'
for a listener added that late, so a HEAD request for it never got an
answer. As master now does for Express (#18234), the response also ends on
'end', or right away when the source has already ended.

Co-authored-by: Roman Khegay <rk@garuna.dev>

* fix(node): abort the response when a streamable file fails midway

Since #18220, the default StreamableFile error handler destroys a response
whose headers are out, but the response object the Node adapter handed it
had no destroy(), so it fell back to end() and a file that failed midway
reached the client as a complete 200. The response now has destroy().

Co-authored-by: Roman Khegay <rk@garuna.dev>

* fix(node): handle async request and response hook errors

A request hook that threw or rejected was fire and forget, so an async hook
throwing an UnauthorizedException took the process down with an unhandled
rejection, done(err) still routed the request, and a second done() ran the
handler twice. As the ExpressAdapter does since #18110, the first
completion routes the request or hands its error to the error handlers,
and later errors are logged; a failing response hook is logged too.

Co-authored-by: Roman Khegay <rk@garuna.dev>

* fix(node): resume the error handlers after the one that failed

handleError() always started over from the first layer, so an exception
filter answering with res.sendFile() looped without end when that call
failed as well (a global @Catch() error page and "Range: bytes=99999-" ran
the filter about 44k times in 1.5 s). Like Express' next() does, it now
resumes after the error handler that ran last, and the final handler
answers with a 416 there.

Co-authored-by: Roman Khegay <rk@garuna.dev>

* fix(node): pass res.locals to the views

The res.locals a middleware set never reached a @Render() view, while
Express and @fastify/view merge them into the view data. The data the
handler returns still wins over them, and they win over the engine's
defaultContext, as with Express' res.render().

Co-authored-by: Roman Khegay <rk@garuna.dev>

* fix(node): parse a body with an empty content-encoding or charset

An empty Content-Encoding header got a 415 and an empty charset parameter
was taken as a charset, while Express and Fastify parse such bodies:
body-parser reads an empty encoding as "identity" and an empty charset as
the default one, and so do the Node adapter's parsers now.

Co-authored-by: Roman Khegay <rk@garuna.dev>

* perf(node): skip the qs decoder for form tokens without escapes

The decoder of "qs" replaces "+" and calls decodeURIComponent() for every
key and value of a urlencoded body. A token without "%" or "+" stays as it
is, so it is now returned right away: qs.parse() of a 10-field form runs
1.7x as fast when no field is escaped, and 1.3x as fast with three escaped
fields, with the same output.

Co-authored-by: Roman Khegay <rk@garuna.dev>

* perf(node): decode middleware path parameters only when escaped

decodeURIComponent() is a call into the runtime (about 80 ns) even for a
value without any "%" to decode, which is most of them. The parameters of
middleware paths are now returned as they are in that case.

Co-authored-by: Roman Khegay <rk@garuna.dev>

* test(node): run the cors and middleware spec on the node adapter

The spec #18217 added for Fastify passes on the Node adapter as it is: enableCors() registers the cors middleware ahead of Nest middleware there, as on Express.

* fix(node): leave the text before a wildcard out of it

A wildcard after text in its segment ("v-*rest", or "ab*cd") also matched
that text alone, so GET /files/v- ran the handler without the middleware
of the route, and the value kept the text (['v-abc', 'def']). The
wildcard now starts after it, as with path-to-regexp and Express, and the
route stays in the tree, ahead of a parameter of the same segment. After
a parameter ("/tags/:id-*rest"), where it starts varies, so such a route,
which never matched, is now matched with path-to-regexp.

* fix(node): match routes for every method by specificity

Routes for every method lived in a tree of their own, merged with the
method routes by registration order, so an @All('*') fallback served
GET /cats of a module registered after it. Each method now has one tree,
which also holds the routes for every method and, for HEAD, the GET
routes, so the most specific route wins whatever its method, as with
Fastify. A single find() replaces the HEAD and @All() lookups.

* fix(node): keep catch-all routes with an optional wildcard in the tree

A route with a variant ending with "/" ("/files/{*path}", what the legacy
"files/*" becomes) was only matched with path-to-regexp once the tree
found nothing, so it lost to any other route: GET /files/readme went to
":section/:id", and GET /api/users to an earlier "{*path}" SPA route. Its
other variants now go in the tree, and path-to-regexp only matches the
paths ending with "/", with its own parameters there, as with Express.

* fix(node): try the other routes when one leaves a parameter empty

find-my-way stops at the most specific route, even when it then leaves a
parameter empty, which path-to-regexp and Express never match. For GET
/ext/.gitignore it picked the "/ext/:file.:ext" variant of
"/ext/:file{.:ext}" with an empty "file", and the lookup gave a 404 while
Express answers with { file: '.gitignore' }. Likewise, GET /docs/.env
missed "/docs/*path" next to "/docs/:name.:ext", and GET /files/v-
missed "/files/:id" next to "/files/v-*rest". The first route that
path-to-regexp matches, in registration order, now answers such a
request, as with Express, and "/users//profile" still matches no
"/users/:id/profile" route.

* perf(node): skip the middleware layers and routes of other prefixes

Nest registers a middleware bound to a controller once per route path, so
forRoutes() on 100 controllers ran about 500 path matchers on every
request. lookupRest() ran the matcher of every route after a version miss
(613 RegExp executions with 1000 routes), and so did lookup() for a path
leaving a parameter empty. A layer or route whose literal prefix the path
does not start with now skips the ones registered right after it that
share that prefix, with a single startsWith(): from 30.1k to 69.4k req/s
for the forRoutes() case, and from 19.6k to 48.9k req/s for the version
miss.

* perf(node): decode route parameters only when escaped

As for the middleware paths, decodeParam() and decodeSegment() of the
route table now return a value without "%" as it is, so a wildcard route
no longer calls decodeURIComponent() for each segment of the path (about
5% more req/s for a 5-segment one).

* fix(node): route a path ending with "/" as the path without it

A route with a variant ending with "/" ("/files/{*path}", what the legacy
"files/*" becomes) took every path ending with "/" from the routes
registered after it, as its whole path-to-regexp pattern matched them: GET
/files/readme went to "/files/readme", but GET /files/readme/ to the
catch-all. It now only takes the paths its variants ending with "/" match
("/files/"), so "/admin/" still goes to an "/admin/{*path}" registered
before "/admin", as with Express, and any other path ending with "/" goes
to the route it would go to without it, as exclude() assumes.

* perf(node): try only the routes that can match an empty segment

When find-my-way's route leaves a parameter empty, lookup() tried every
route with path-to-regexp, so GET /acme/res500//profile took about 190 µs
with 2000 "/:tenant/..." routes, and 15 µs with distinct prefixes. A
parameter or wildcard that is a whole segment of its route is only empty
for "/" or a path with an empty segment ("//"), which only routes with a
wildcard, a variant ending with "/" or "//" can match: those are now the
only ones tried, and none for "/" (about 200 ns either way). Routes with
a segment holding text and a parameter ("/ext/:file.:ext") or a wildcard
after text still try every route.

* test(node): cover trailing bytes after a compressed body

Ports body-parser's tests for bytes that arrive after a compressed frame,
which zlib ends the decompression stream on since Node.js 11: the request
still finishes, a late decoder error is swallowed instead of ending the
process, and a body rejected for its charset never creates a decoder. The
parser already keeps an error listener on the decoder once it has read the
body; without it, these tests report the decoder's error as uncaught.

* fix(node): copy the multer fixes of platform-express again

The multer interceptors are copies of platform-express' (the file upload
parity spec checks it). Two fixes landed there since: a multipart request
with an unsupported content type is answered with a 415 (#18264), and
isRejectedByPipes() is no longer a named import from @nestjs/core/internal,
which made the package fail to load with a core older than 12.2 (#18251).

* fix(node): update multer to 2.5.0 and find-my-way to 9.10.0

As platform-express (#18268) and platform-fastify pin them. multer 2.5.0
fixes GHSA-q7qp-rjg7-q26m: a request aborted while an asynchronous
fileFilter is pending could crash the process.

* test(node): run master's new spec cases on the node adapter

The Node twins of the disconnect specs now wait for the producer teardown
as the Express and Fastify ones do since #18260, which fixes the same
off-by-one tick under load. getUrl() is checked for an https server
(#18252), and the specs of #18276 for middleware on routes that differ
only in their version run on the Node adapter too; both pass as they are.

* feat(node): add an option to trust proxies

Behind a reverse proxy, req.ip was always the proxy's address and
req.protocol "http" behind one that terminates TLS, so the throttler
counted every client as one, express-session would not set secure
cookies, and passport built http:// callback URLs. As Express' "trust
proxy" setting and Fastify's option of the same name, new NodeAdapter({
trustProxy }) takes true, a hop count, addresses or subnets, or a
function. For a request from a trusted proxy, req.ip, req.ips,
req.protocol, req.secure and req.hostname are read from the
X-Forwarded-* headers the way Express reads them, and
req.app.get('trust proxy') returns the setting. Unlike Fastify, which no
longer trusts a hop count, the adapter trusts it as Express does.

Without the option, the requests are NodeRequests as before: the getters
that read the headers belong to a subclass the option creates, so the
default requests run the same code. req.ips is new (empty there), and
req.protocol is typed as a string, as with Express.

* feat(node): add an option to parse the query string

The Node adapter parses query strings with fast-querystring into flat
keys, as Express and Fastify do by default, but offered no way to parse
nested objects and arrays, which Express gets from its "query parser"
setting and Fastify from its querystringParser option. As with Fastify,
new NodeAdapter({ querystringParser }) takes a function the query string
is passed to (without the "?", and "" when the URL has none), such as
str => qs.parse(str). Without it, requests run the same code as before.

* fix(node): trust no proxy for a hop count, as fastify does

A hop count cannot tell the socket's peer from a client that sends its
own X-Forwarded-For, so Fastify 5.12.1 and later trust no proxy for one,
and leave numbers out of the type of its trustProxy option. The Node
adapter now does the same, instead of trusting that many hops as Express
does. Addresses, subnets, true and functions are unchanged.

---------

Co-authored-by: Roman Khegay <rk@garuna.dev>
2026-10-27 09:59:47 +01:00
Kamil Myśliwiec 10adb73c57 fix(core): check the sse pending data limit after each write
An SSE client whose stream is backed up is dropped once more event data
waits for it than the route's "maxPendingDataLength" allows. The limit
was only checked when the producer emitted an event. A producer that
emits many events at once, synchronously (e.g. replaying past events
before live ones), emits them all before the first write can back the
stream up, and none after, so they waited in memory for a client that
never read them, past the limit.

The limit is now also checked after each write, once the stream is
backed up. A route that sends more than its limit at once, to a client
that reads slower than the events are written, now drops that client
as well, as the limit describes. Such a route can raise or lift it.
2026-10-27 08:48:03 +01:00
Denis Scarabelli f991703156 fix(common): report the detected file type in file type errors (#18289) 2026-10-27 08:44:07 +01:00
renovate[bot] 870c54b44d fix(deps): update graphql-tools monorepo (major) (#18283) 2026-10-26 22:41:51 +01:00
Kamil Mysliwiec b7a891ee73 chore(deps): update undici to v8.12.3 and fast-uri to v3.1.9 (#18286) 2026-10-26 22:28:53 +01:00
renovate[bot] 66652ab239 fix(deps): update graphql-tools monorepo (#18281) 2026-10-26 22:28:41 +01:00
renovate[bot] f7fff4848f fix(deps): update dependency @nestjs/webhooks to v0.0.3 (#18258) 2026-10-26 22:27:58 +01:00
renovate[bot] 6d54083eeb fix(deps): update dependency dotenv to v18.0.9 (#18259) 2026-10-26 22:27:14 +01:00
renovate[bot] 2c12e2b7d7 fix(deps): update dependency mercurius to v16.12.1 (#18280) 2026-10-26 22:26:31 +01:00
renovate[bot] c0284c3c21 fix(deps): update dependency fast-json-stringify to v7.2.0 (#18274) 2026-10-26 22:16:40 +01:00
Om Bhandwaldar f640806541 fix(core): run middleware once for routes that differ only in version (#18276) 2026-10-26 22:14:31 +01:00
Kamil Mysliwiec 6aafca1fd8 fix(ws,microservices): load packages where bundlers can see them (#18285)
* fix(ws): import ws statically so bundlers include it

ws is a dependency of @nestjs/platform-ws, but the adapter loaded it
with createRequire(import.meta.url)('ws') in its constructor, which
esbuild and rolldown don't follow: a bundled app left ws out and exited
with "ws package is missing". A static import is bundled by all of them.

* fix(microservices): load transport packages where bundlers can see them

ClientMqtt, ClientRMQ, ClientNats, ServerGrpc and ServerRMQ loaded their
packages with createRequire() in the constructor, which esbuild and
rolldown don't follow. They now load them with a literal import() when
the client connects or the server listens, as ServerKafka and
ServerRedis already do. createClient() of ClientMqtt and ClientRMQ still
loads the package when called before connect().

ClientGrpcProxy keeps its synchronous load, since getService() is
synchronous; bundled applications have to mark its packages external.

* docs(common): note which bundlers resolve synchronous package loaders

webpack 5 follows createRequire(import.meta.url)('pkg'), esbuild and
rolldown don't. Point to a literal import() loader or a static import.
2026-10-26 15:11:18 +01:00
Kamil Myśliwiec d22065e0d5 fix(microservices): bound how long a tcp packet may take to arrive
A packet's declared length was never compared against "maxBufferSize",
so a packet that could never be accepted was still buffered until the
buffer filled up. It is now rejected as soon as its length is read, and
the connection is dropped, since everything that follows is the rest of
that packet.

The idle timer for a partially received packet is refreshed on every
read, so a peer that sends a few bytes now and then kept a partial
packet buffered for as long as it liked. The new "messageReceiveTimeout"
option (default: 300000ms, like "requestTimeout" in node:http) bounds
the time from a packet's first character to its last, and is not
extended by further reads. Time spent with reading suspended due to
backpressure is not counted. Set it to 0 to disable it.

Also corrects the comment on the default "maxBufferSize": it is counted
in characters, which take 1 or 2 bytes each, not 4.
2026-10-26 09:02:24 +01:00
renovate[bot] af957c9a67 chore(deps): update mysql docker tag to v26.10.0 (#18273) 2026-10-25 21:27:46 +01:00
renovate[bot] ef940a16f3 fix(deps): update dependency hbs to v4.4.0 (#18275) 2026-10-25 21:27:04 +01:00
renovate[bot] 27180cb194 chore(deps): update dependency webpack to v5.112.2 (#18271) 2026-10-25 21:26:21 +01:00
renovate[bot] 5caa6c7968 fix(deps): update dependency mongoose to v9.11.4 (#18270) 2026-10-25 21:16:40 +01:00
dependabot[bot] eb7fea2862 chore(deps): bump fast-uri in /tools/benchmarks (#18265) 2026-10-25 21:06:27 +01:00
dependabot[bot] f74297c850 chore(deps): bump body-parser from 2.3.0 to 2.4.0 in /tools/benchmarks (#18266) 2026-10-25 21:05:43 +01:00
renovate[bot] 5b91e5b8c9 chore(deps): update dependency body-parser to v2.4.0 [security] (#18267) 2026-10-25 21:05:01 +01:00
renovate[bot] 0eef54172d fix(deps): update dependency multer to v2.5.0 [security] (#18268) 2026-10-25 20:53:07 +01:00
Junpei Zhang 3eb55705a6 docs: fix subject-verb agreement in contributing guide (#18269) 2026-10-25 20:52:03 +01:00
Om Bhandwaldar 11e1466b96 perf(microservices): parse message patterns only when they may be json (#18277) 2026-10-25 20:51:57 +01:00
Maksym Lyskov ff84deb156 fix(common): keep one route param per argument in an overriding method (#18272) 2026-10-25 20:51:50 +01:00
snowingfox 3e2309e918 fix(core): tolerate sse request sockets without tuning methods (#18262) 2026-10-24 20:41:50 +02:00
yooseong 0c9db73505 fix(express): map unsupported multipart content types to 415 (#18264) 2026-10-24 20:41:42 +02:00
Kamil Mysliwiec 50abd26d5b test(core): wait for the producer teardown in the disconnect specs (#18260)
The disconnect specs read the tick count right after the client destroyed its socket, but the server only sees the disconnect once the socket close reaches it. Under load the producer's 20 ms interval could tick once more in between, so the count was off by one (7 of 12 runs failed under full CPU load).

The producer now records its teardown, and the specs take the count once it has happened. They still fail when the producer is never stopped.
2026-10-24 20:22:00 +02:00
renovate[bot] ac26786497 fix(deps): update nest monorepo (#18141) 2026-10-24 15:30:28 +02:00
renovate[bot] b5ea32b127 chore(deps): update dependency oxlint to v1.89.0 (#18205) 2026-10-24 15:13:08 +02:00
renovate[bot] bb54a295ea chore(deps): update dependency typescript-eslint to v8.72.0 (#18237) 2026-10-24 15:12:27 +02:00
renovate[bot] 0993247d97 chore(deps): update dependency @fastify/multipart to v10.1.6 (#18242) 2026-10-24 15:10:03 +02:00
renovate[bot] 3ec329f72b fix(deps): update dependency @nestjs/outbox to v0.1.3 (#18243) 2026-10-24 14:58:17 +02:00
renovate[bot] 772bd1c661 fix(deps): update dependency @nestjs/swagger to v12.0.4 (#18244) 2026-10-24 14:58:12 +02:00
Kamil Mysliwiec 28113250fa fix(deps): update dependency file-type to v22.2.0 (#18257) 2026-10-24 14:52:46 +02:00
Maksym Lyskov 41030750ef fix(fastify): send object error bodies with a json content type (#18253) 2026-10-24 14:35:53 +02:00
Roman Khegay 0fb57c8b07 fix(core,express,fastify,ws,socket.io): load adapters with an older core (#18251) 2026-10-24 14:35:13 +02:00
Roman Khegay 135f999b6d fix(core): read the url protocol from the server (#18252) 2026-10-24 14:22:58 +02:00
Roman Khegay 79b57f962c fix(core): name the array and index of undefined module entries (#18254) 2026-10-24 14:22:50 +02:00
Abdellatif Anaflous ddb753accd fix(microservices): re-arm the redis retries on a new listen (#18250) 2026-10-24 14:22:42 +02:00
Roman Khegay ce57692f1f fix(microservices): report a nats connection that gave up reconnecting (#18255) 2026-10-24 14:22:34 +02:00
Roman Khegay 2b6bacb71a fix(microservices): do not emit a trailing null to kafka requesters (#18249) 2026-10-24 14:22:26 +02:00
Abdellatif Anaflous e5b2436946 fix(microservices): let a retriable exception keep travelling (#18256) 2026-10-24 14:22:17 +02:00
fengxueweilai 9a95605281 fix(common): avoid throwing for arrow prototype constructors (#18246) 2026-10-23 09:19:53 +02:00
Kamil Myśliwiec c828ad160f fix(microservices): treat an error in an rpc exception as unknown
BaseRpcExceptionFilter sent the error held by an RpcException as it is
whenever it was an object. For an Error, as in new RpcException(err),
the client then got the enumerable properties of the Error (such as the
request config of an HTTP client error) or its toJSON() output.

An Error held by an RpcException is now handled as an unknown error: it
is logged, and the client gets the "Internal server error" payload.
Plain objects and strings are sent as before.
2026-10-22 19:56:10 +02:00
Kamil Myśliwiec fcd79249b1 fix(websockets): treat an error in a ws exception as unknown
BaseWsExceptionFilter sent the error held by a WsException as it is
whenever it was an object. For an Error, as in new WsException(err),
the client then got the enumerable properties of the Error (such as the
request config of an HTTP client error) or its toJSON() output.

An Error held by a WsException is now handled as an unknown error: it is
logged, and the client gets the "Internal server error" payload, with
its cause. Plain objects and strings are sent as before.
2026-10-22 19:55:55 +02:00
Roman Khegay 62765431ec fix(fastify): leave 5xx fastify errors unmapped so they are logged (#18240) 2026-10-22 12:26:53 +02:00
Spoko Dev cb4d9ffae6 fix(express): end a head response for an empty streamable file (#18234) 2026-10-22 09:49:09 +02:00
Kamil MysliwiecandAbdel 8abaa0e61f fix(fastify): map 4xx http-errors errors to http exceptions (#18239)
An http-errors error that an Express middleware passes to next() on Fastify (e.g. PayloadTooLargeError from body-parser) reached exception filters as a raw error, so a @Catch(HttpException) filter never ran for it. Errors below 500 are now mapped to an HttpException, as on Express. A 5xx one stays unmapped, so the built-in filter still logs it.

Co-authored-by: Abdel <62770500+hktitof@users.noreply.github.com>
2026-10-22 09:26:49 +02:00
renovate[bot] 18a93264d7 fix(deps): update dependency mongoose to v9.11.3 (#18236)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-22 09:18:45 +02:00