The built-in body parsers call next() from the "end" event of the
request, which Node.js emits in the async context of the socket. For
every request with a body, the handler, the interceptors and the
response hook therefore lost an AsyncLocalStorage store that a request
hook or an earlier middleware entered, as tracing and request-context
libraries do. body-parser (through raw-body) and the content type
parser of Fastify bind that callback to the context they were called
in, so the store survives on Express and Fastify.
A parser that has a body to read now binds next() to the context it
was called in. It does so with an AsyncResource and runInAsyncScope(),
as the interceptors of @nestjs/core do, rather than with
AsyncResource.bind(), which is about 25 times slower: it adds about
45 ns to a request with a body, and nothing to one without.
find-my-way reads the first character of a route path as its leading "/",
so the "{*any}" route @nestjs/serve-static registers for the fallback of
a single-page app became find-my-way's "*" route, whose wildcard the
route table then rejected for every request: client-side routes got a
404. A route path that does not start with "/" is now matched with
path-to-regexp, as Express matches it ("{*any}" matches every path, and
"users" never matches "/users"). Routes Nest registers always start with
"/" and stay in the tree.
* 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>
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.
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.
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.
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>
Since #18214, every http-errors error that reaches the Express error handler is mapped to an HttpException, which the built-in exception filter does not log. Server faults such as EACCES from serving a static file, or a 5xx a middleware passes to next(), were answered without a log line. Only errors below 500 are mapped now, so a 5xx one is logged again, as on Fastify.
Co-authored-by: rk <rk@garuna.dev>
With the "strict routing" setting, the router serves the root route of a controller without a trailing slash ("/users"), while the middleware of forRoutes() was registered with one ("/users/") and did not run for it. The path is now registered without the trailing slash too.
The request hook of app.enableCors() and the cors option now runs before Nest middleware, as on Express: a preflight is answered before them, and the responses a middleware ends or fails with carry the CORS headers. A cors hook other than onRequest is registered as before. An options delegate that takes (request, callback) now works.
Closes#18212
Middleware and CSRF exclusions now also cover the routes excluded from the global prefix that they match, which the router serves without the prefix. Such a route now skips the middleware or the CSRF check, as the same exclusion already does for routes under the prefix.
- Read abortOnError from the select() options in the exception proxy and wrap the selected
context with that value, so its get() and further select() calls exit or throw as requested.
- Leave the call and its result unchanged when select() gets no abortOnError.
- Cover both directions, a failing select(), nested selects and the default in an e2e spec.
The middleware exclusions read the version of an Accept header with a
private copy of the function the platform packages use, and that copy
split the header on every comma and semicolon, so a quoted parameter
value was cut in the middle. A request for "application/json;v=\"1\""
was read as no version at all, so the excluded route did not match and
the middleware ran on a request the router serves with the excluded
version.
The copy now splits the header outside quoted strings and unescapes a
quoted version, exactly like the platform packages do since the media
type version helpers were fixed, so the version the exclusions match is
the version the router serves.
Co-authored-by: hktitof <titofe06@gmail.com>
* fix(core): resolve request-scoped forward refs for concurrent requests
Since #18186, concurrent requests that resolve a two-sided forwardRef()
pair of request-scoped providers can fail with a false
CircularDependencyException. Each load creates its own settlement
signal, but it was only stored on the shared wrapper, so the pending
check, the recorded cycle edges and the deferred-load failure used the
signal of whichever context loaded the wrapper last.
Each per-context instance now keeps its own signal, and the three sites
use the signals of the context being resolved.
* perf(core): record settlement edges with a small class
An object literal with a getter on every unresolved edge made resolving request-scoped chains about 12% slower. Also cover resolving both sides of a one-sided cycle at once.
---------
Co-authored-by: Kamil Myśliwiec <mail@kamilmysliwiec.com>
* test(express): cover an exception whose response is an error
ExpressAdapter.reply() hands an Error body, as in
new UnauthorizedException(err), to response.json(), which serializes
the enumerable properties of the Error (such as the request config of
an HTTP client error, with its authorization header) with the status of
the exception. The FastifyAdapter sends the standard body of the status
instead.
The new unit and e2e specs expect the body the FastifyAdapter sends, so
they fail until the ExpressAdapter does the same.
* fix(express): send the standard body for an exception wrapping an error
ExpressAdapter.reply() handed an Error body, as in
new UnauthorizedException(err), to response.json(), which serializes the
enumerable properties of the Error with the status of the exception. For
an HTTP client error that is its request config, including the
authorization header the request was sent with.
An Error body sent with an error status now gets the standard body of
that status instead, e.g. {"statusCode":401,"message":"Unauthorized"},
as the FastifyAdapter sends since 614d1ac7e, so both adapters answer
alike.
The two WsAdapter specs with two gateways sent a message on their second
WebSocket as soon as the first one had answered, without waiting for the
second one to open. When it was still connecting, e.g. on a loaded
machine, send() threw "WebSocket is not open: readyState 0
(CONNECTING)". They now wait for its "open" event, listened for right
after the socket is created so that it can't be missed.
FastifyAdapter.reply() handed an Error body, as in
new UnauthorizedException(err), to Fastify, which passes an Error
payload to the error handler instead of sending it. Routes then answered
500 rather than the status of the exception, and on the raw response
that middleware gets, which has no error handler, the send threw.
An Error body sent with an error status now gets the standard body of
that status instead, e.g. {"statusCode":401,"message":"Unauthorized"},
from routes and middleware alike. The properties of the Error (such as
the request config of an HTTP client error) are not sent, whereas the
ExpressAdapter serializes them.
Discovered by Anthropic's OSS Scanner, as vulnerability ANT-2026-3CVCDVPD.
A microservice connected with deferInitialization assigned the clients
and called onModuleInit() and onApplicationBootstrap() on the container
it shares with the application when it was started, and the application
did it again when it was initialized, so both hooks ran twice, in either
order. When the application was initialized first, the @Client()
properties also kept the microservice's clients, which were not closed
with the application.
A connected microservice now only registers its listeners when it is
started; the application assigns the clients and calls the hooks, as
it already did for microservices connected without deferInitialization.
As for those, a deferred microservice started before app.init() or
app.listen() now serves messages before the hooks have run and the
clients are assigned (initialize the application first to keep the
previous order), and an application that is never initialized gets
neither.
NestMicroservice.init() initialized the application context, which
calls onModuleInit() and onApplicationBootstrap(), and then registered
the modules, which calls them again. A microservice started with init()
therefore ran both hooks twice, the first time before its gateways,
clients and listeners were set up.
init() now only registers the modules, as listen() does, so the hooks
run once, after that setup, and see the @Client() properties and the
@WebSocketServer() servers, as they do on listen().
NestFactory.create() registers @fastify/middie before main.ts runs, and
middie gives each new encapsulated context a copy of the middleware of
its parent as it is at that time. A plugin registered with an awaited
register() before app.init() therefore got a context without the
middleware app.init() registers through MiddlewareConsumer, so its
routes ran without it, unlike with a register() that is not awaited or
with Test.createNestApplication().
The adapter now tracks the contexts created since middie was registered
and also registers each such middleware in them, without its path, then
matches the path and strips it from req.url itself, as middie does.
Routes of plugins registered before app.init() now run the middleware
bound to their paths. In those contexts it runs after the plugin's own
middleware, and app.use() middleware added after the register() still
does not reach them.
Discovered by Anthropic's OSS Scanner, as vulnerability ANT-2026-5OW4PN2N.
* fix(express,fastify): parse quoted media type version parameters
* fix(express,fastify): split quoted headers in linear time
* style(express,fastify): format media type version helpers
* refactor(express,fastify): add document media type parsing
* style(express,fastify): revert code style
* style(express,fastify): revert code style
* fix(express,fastify): read a version before an unterminated quote
An unterminated quoted string now runs to the end of the header value instead of discarding the whole value, so a version parameter before it is read as before this change, while one after its opening quote is still not.
---------
Co-authored-by: Kamil Myśliwiec <mail@kamilmysliwiec.com>
* fix(fastify): keep a raw-response set-cookie value when appending
appendHeader() dropped a Set-Cookie value that a middleware wrote on
the raw response, because Fastify replaces the raw headers with the
reply headers when reply.send() writes them. appendHeader() now reads
the current value with reply.getHeader(), which also reads the raw
response, and writes the merged list to the reply and to the raw
response. Every cookie is sent, as on Express, through reply.send(), a
hijacked reply, or a handler that ends reply.raw itself.
The raw response is updated only while its headers are not sent, like
reply.removeHeader() does.
* fix(fastify): accept the raw response in the header helpers
A middleware gets the raw response, so calling setHeader(),
appendHeader() or setCookie() there threw "response.header is not a
function". They now also accept the raw response, like status(), end()
and reply() already do.
On the raw response, appendHeader() reads the current value and sets the
merged list, because Http2ServerResponse only has appendHeader() since
Node 20.12.
* fix(fastify): copy the set-cookie list written to the raw response
The reply and the raw response held the same array, so a later reply.header() call changed the raw header too.
---------
Co-authored-by: Kamil Myśliwiec <mail@kamilmysliwiec.com>
With caseSensitive: false, the router decodes the request path and then
lower-cases it, so "/tas%E2%84%AAs" (U+212A KELVIN SIGN lower-cases to
"k") was served by the "/tasks" route without the route's middleware.
sanitizeUrl() lower-cased the path before decoding it, and the
case-insensitive RegExp @fastify/middie matches paths with does not
fold such characters.
The path is now decoded before it is lower-cased. On a case-insensitive
router, the middleware is registered with middie for every path and
only the adapter matches it, stripping the path from req.url as middie
does. Such requests now run the middleware of the route that serves
them, and every middleware path is checked for each request there.
Discovered by Anthropic's OSS Scanner, as vulnerability ANT-2026-GII797H1.
Fastify calls the extractor of VersioningType.CUSTOM while it routes the
request, before there is any hook or error handler, so an exception it
threw (e.g. new URL() on a malformed Host header) left the HTTP server's
request listener and ended the process.
The exception is now kept for the request and passed to the error
handler from an onRequest hook that runs before Nest middleware, so the
exception layer answers it (500, or the status of an HttpException) as
on Express. The extractor runs for every request on Fastify, so this
applies to unmatched routes as well.
Discovered by Anthropic's OSS Scanner, as vulnerability ANT-2026-49I1K0U0.
The wrapper the adapter registers with @fastify/middie returned the
promise of Nest's middleware proxy, which middie discards. When the
exception layer threw while answering an error thrown by a middleware
(e.g. an HttpException whose body is an Error, which cannot be sent on
the raw response middleware gets, or a filter calling FastifyReply
methods), the rejection was unhandled and the process exited.
The rejection is now passed to next(), once, so Fastify's error handler
answers such a request with a 500, as it does for guards and route
handlers, instead of the process exiting. A rejection that follows a
call to next() is logged.
Discovered by Anthropic's OSS Scanner, as vulnerability ANT-2026-3CVCDVPD.
GrpcExceptionFilter is a catch-all RPC exception filter: it returns an
error stream for the transport to send. A hybrid application that
registers it globally and lets its gRPC microservice inherit that
configuration (as the gRPC e2e spec does) also bound it to every HTTP
route and to the not-found handler, where ExceptionsHandler took the
filter's match as handled and nobody subscribed to the stream. No HTTP
error was answered or logged, and requests pipelined on one connection
piled up until the process ran out of memory.
ExceptionsHandler no longer counts a filter that returns an Observable
as having handled the exception, unless the response has been sent, so
the request gets the default response. This covers GrpcExceptionFilter
and any other RPC exception filter bound to HTTP routes. Filters that
throw are handled as before.
Behaviour change: an HTTP request whose exception filter returns an
Observable without answering it is now answered by the built-in filter
instead of being left unanswered.
Discovered by Anthropic's OSS Scanner, as vulnerability ANT-2026-C2280XED.
A microservice connected with deferInitialization initialised itself
when it was started and bound every gateway of the container it shares
with the application a second time: with its own configuration, so
without the application's global guards, pipes and interceptors, and
without an HTTP server, so a gateway on the default port also got a
Socket.IO server of its own on a random port on all interfaces, which
app.close() left open.
Connected microservices no longer bind gateways; the application does,
as it already did for microservices that are not deferred. The extra
listener is gone, afterInit() runs once, a gateway on an explicit port
no longer fails with EADDRINUSE, and with inheritAppConfig a gateway on
the default port is served on the HTTP server again.
Discovered by Anthropic's OSS Scanner, as vulnerability ANT-2026-AV0SSM6Q.
A default-scoped gateway with scoped global enhancers loads them itself,
per connection, after resolving its own context. A request-scoped
provider with a forwardRef() parameter that one of them depends on was
loaded without anyone waiting for it, so the message handler ran with
its never-constructed placeholder, and an exception thrown by its
constructor (e.g., WsException('Unauthorized')) was discarded.
The request-scoped handler now waits for the deferred loads of the
connection's context before it builds the handler, as loadPerContext()
does, so such an exception now reaches the gateway's exception filters
instead of the handler running.
Discovered by Anthropic's OSS Scanner, as vulnerability ANT-2026-MTEH0A9T.
A default-scoped gateway with no scoped global enhancers builds its
message handlers once, in the static context and without an inquirer
id, and the injector constructed the transient enhancers of a static
provider for entry providers only. A transient guard bound to such a
gateway with @UseGuards() was then looked up as the placeholder whose
constructor never ran. With an arrow-function canActivate it was
skipped, so the handler ran for any client; with a prototype method it
ran without its dependencies.
The bootstrap load of every static provider now constructs its
transient enhancers with the provider as the inquirer, and the static
handlers of a gateway are created with its id as the inquirer, as those
of controllers and entry providers are. Such guards now run.
Discovered by Anthropic's OSS Scanner, as vulnerability ANT-2026-J6VYP0AI.
ExternalContextCreator#create() looked up the module of a handler's
host by its class, so a GraphQL resolver registered under a token other
than its class (e.g., `{ provide: 'PAYROLL', useClass: Resolver }`) was
found in no module. Its own guards, pipes, interceptors and filters were
then dropped without an error, and no transient global guard ran on it
either, as no inquirer could be derived for it.
When no module has the class as a provider token, the module whose
providers hold the host is used instead: the one with the given
inquirer id, or else the one whose instance it is. A static entry
provider is also recognized by its instance when its inquirer is
derived. Such resolvers now run their own guards, and transient global
guards when static.
Discovered by Anthropic's OSS Scanner, as vulnerabilities ANT-2026-2G8PZ4KH and ANT-2026-J6VYP0AI.
Fastify answers a HEAD request to a GET route through its HEAD route,
whose onSend handler resumes a stream payload instead of sending it.
For the stream of a StreamableFile, that read the whole source at full
speed with nothing consuming it. A route declared for HEAD piped the
stream into a response that discards it, with the same result. Each
HEAD request cost as much disk and CPU time as a full download, without
the client having to receive anything.
For a HEAD request, reply() now destroys the source before anything is
read and hands Fastify an empty stream. Unlike an empty body, for which
the HEAD route resets Content-Length to 0, the empty stream keeps the
Content-Type, Content-Disposition and Content-Length of the file.
HEAD requests to a GET route get the same status and headers as before,
also when the source cannot be opened (200, where GET gets a 500). A
route declared with @Head() now answers such a source the same way,
instead of with a 500. GET requests are unchanged.
Discovered by Anthropic's OSS Scanner, as vulnerability ANT-2026-XZ3CL5Q9.
With header, media type or custom versioning, the version is not part
of the path, and exclude({ path, method, version }) dropped it: an
exclusion for version 1 skipped the middleware for every version of
that path, so a client reached another version's handler without it
just by naming that version. enableCsrfProtection({ exclude }) widened
the same way.
Such an exclusion now applies only when the request asks for one of its
versions, read the way the adapters' version filters read it: the
header, the Accept parameter, or the custom extractor, which therefore
runs a second time for these requests. A request for several versions
is excluded only if all of them are versions of the exclusion, and one
whose extractor throws is not excluded. Version-neutral exclusions and
URI versioning are unchanged.
Discovered by Anthropic's OSS Scanner, as vulnerability ANT-2026-DN0IZWM7.
With setGlobalPrefix(prefix, { exclude: ['x/{*path}'] }) (or a legacy
'x/*'), the router tests the root route of @Controller('x') as "/x/",
which the exclusion matches, and serves it at "/x", outside of the
prefix. The middleware was only mounted on the exclusion's own pattern,
which does not match "/x", so neither a wildcard forRoutes() nor
forRoutes('x') ran for "GET /x".
Such middleware is now also mounted on "/x", where the router serves
that route. A request for "/x" that no route serves now gets the
middleware's response instead of a 404.
Discovered by Anthropic's OSS Scanner, as vulnerability ANT-2026-P8IB7YGY.
An exclusion ending in an optional wildcard ("x/*", "x/(.*)" or
"x/{*path}") matches "/x/" but not "/x". On a router that ignores a
trailing slash (Express by default, Fastify with ignoreTrailingSlash),
"/x/" is served by the "/x" route, so MiddlewareConsumer.exclude()
skipped the middleware, and enableCsrfProtection({ exclude }) the
cross-origin check, for that parent route: "GET /invoices/" returned
the listing that "GET /invoices" protects.
On such a router, a request path ending in "/" is now excluded only if
the path without the slash is excluded too. A "/x/" served by an
excluded optional-wildcard route itself, such as "GET /files/" for
@Get('files/{*path}'), now runs the middleware and the cross-origin
check, unless "x" is excluded as well or the exclusion is written
"x{/*path}". Routers that match a trailing slash exactly are unchanged.
Discovered by Anthropic's OSS Scanner, as vulnerability ANT-2026-P8IB7YGY.
The router decides for each route whether the global prefix applies,
but forRoutes() decided it once for each path it was given. A path
that covers a route excluded from the prefix, without being excluded
itself, was only mounted under the prefix, while the router served that
route without it: forRoutes('health') with GET "health" excluded, or
forRoutes('users/:id') with "users/me" excluded.
Such a path now also mounts the middleware on the excluded routes it
covers, for the request methods both have. Only the exclusions without
parameters or wildcards are considered. A request for such an excluded
path now runs the middleware even when no route serves it.
Discovered by Anthropic's OSS Scanner, as vulnerability ANT-2026-LJP4ZL1S.
When the root path is excluded from the global prefix, the paths of a
wildcard middleware include "/" next to the prefixed ones, and
registerHandler() then registered "/" alone. Only Express "use()" reads
"/" as every path: a wildcard bound to a method ({ path: '*', method })
went through all() or get() and ran on "/" only, so the whole prefixed
API was served without it. On Fastify, which rewrites a bare "/" to
"<prefix>/{*path}", the other routes excluded from the prefix lost it.
The middleware is now registered on each of its paths, the root path
first so that "use()" still runs it for every request with "req.url"
unchanged, and it runs for the first of them that matches a request
only. This also stops it from running twice for "/api/" on Fastify, and
replaces the Express adapter's own check for "/api/". FastifyAdapter
still rewrites a bare "/", so on Fastify the root path itself is still
served without the middleware.
Discovered by Anthropic's OSS Scanner, as vulnerability ANT-2026-P3SFKBU4.
removeOverlappedRoutes() dropped any route whose path matched a regex
built by hand from a sibling ":param" route, so that the middleware ran
once for them. That regex matched more than the sibling's mount: it
swallowed text after a parameter (":id.json"), read pattern routes
("{*path}", "*path") as literal paths, and ignored the version and the
global prefix exclusions. The dropped route was then covered by no
mount, and its handler ran without the middleware: "/docs/admin" next
to "/docs/:id.json", "/v2/accounts/me" next to "/v1/accounts/:id", or
"/users/me" excluded from the global prefix next to "/users/:id".
A route is now dropped only when it has no parameters or wildcards and
the parameterized route, compiled with path-to-regexp as the adapters
mount it, matches every path the route is mounted on, global prefix
and version included. A request matched by two routes that are now
both kept, such as "/files/a" for "/files/:id" and "/files/{*path}",
runs the middleware twice.
Discovered by Anthropic's OSS Scanner, as vulnerabilities ANT-2026-J26HGV43 and ANT-2026-LJP4ZL1S.
With URI versioning and a defaultVersion, controllers without a version
are served under that version ("/v1/dogs"), but a path passed to
forRoutes() without a version ('dogs', or { path: 'dogs', method }) was
mounted at "/dogs" only, so the middleware never ran for those routes.
Such a path is now also mounted under the default version(s), next to
the bare path that version-neutral controllers are served at. The
routes of a version-neutral controller passed as a class keep
VERSION_NEUTRAL as their version, instead of none, so that they do not
get the default version too. exclude() is unchanged: an excluded path
still does not cover its default-version route.
Discovered by Anthropic's OSS Scanner, as vulnerability ANT-2026-EOL7PA0M.
Under a global prefix or a URI version, a wildcard middleware is
registered on the exact "/api" next to "/api/<wildcard>". As Express
routes are not strict, the exact entry also matches "/api/", and it left
that path to the wildcard entry, assuming an optional "{*path}". A named
wildcard such as forRoutes('*path') needs one more segment, so
"GET /api/" was served by the "/api" handler without the middleware.
A middleware now runs for the first of its paths that matches a request,
and the others skip that request, so "/api/" runs it once through the
exact entry whatever the wildcard. With forRoutes('*') or '{*path}', the
middleware then sees req.url "/api/" for that request, as for "/api",
instead of "/" with req.baseUrl "/api".
Discovered by Anthropic's OSS Scanner, as vulnerability ANT-2026-08GP7DTU.
The router registers a controller once for each module that declares
it, under the path that module is mounted at with RouterModule, but
forRoutes(Controller) mapped it under the first RouterModule target
only. Its routes under the other modules (or under a module without a
RouterModule path) were served without the middleware, depending on
the module order.
The controller is now mapped under the path of every module that
declares it, so the middleware runs on all of them. A controller passed
to exclude(), which the typings do not allow, is mapped the same way.
Discovered by Anthropic's OSS Scanner, as vulnerability ANT-2026-BE9QEOAW.