@nestjs/platform-express 12.1.1 → 12.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@ import * as http from 'http';
5
5
  import * as https from 'https';
6
6
  import { NestExpressBodyParserOptionsFor, NestExpressBodyParserType } from '../interfaces/nest-express-body-parser.interface.js';
7
7
  import { ServeStaticOptions } from '../interfaces/serve-static-options.interface.js';
8
- import { type CorsOptions, type CorsOptionsDelegate, type VersionValue } from '@nestjs/common/internal';
8
+ import { type CorsOptions, type CorsOptionsDelegate, type RouteInfo, type VersionValue } from '@nestjs/common/internal';
9
9
  import type { NestApplicationOptions } from '@nestjs/common';
10
10
  import { AbstractHttpAdapter } from '@nestjs/core';
11
11
  type VersionedRoute = <TRequest extends Record<string, any> = any, TResponse = any>(req: TRequest, res: TResponse, next: () => void) => any;
@@ -21,7 +21,18 @@ export declare class ExpressAdapter extends AbstractHttpAdapter<http.Server | ht
21
21
  private onRequestHook?;
22
22
  private onResponseHook?;
23
23
  constructor(instance?: any);
24
- setOnRequestHook(onRequestHook: (req: express.Request, res: express.Response, done: () => void) => Promise<void> | void): void;
24
+ private runRequestHook;
25
+ /**
26
+ * Registers a request hook. Call `done()` to continue processing the request;
27
+ * resolving the returned promise does not continue it. Only the first
28
+ * completion is forwarded to Express. Errors after `done()` are logged
29
+ * without advancing the request again.
30
+ */
31
+ setOnRequestHook(onRequestHook: (req: express.Request, res: express.Response, done: (err?: Error) => void) => Promise<void> | void): void;
32
+ /**
33
+ * Registers a hook to run after the response has been sent.
34
+ * Thrown errors and rejected promises are logged.
35
+ */
25
36
  setOnResponseHook(onResponseHook: (req: express.Request, res: express.Response) => Promise<void> | void): void;
26
37
  reply(response: any, body: any, statusCode?: number): any;
27
38
  status(response: any, statusCode: number): any;
@@ -29,7 +40,7 @@ export declare class ExpressAdapter extends AbstractHttpAdapter<http.Server | ht
29
40
  render(response: any, view: string, options: any): any;
30
41
  redirect(response: any, statusCode: number, url: string): any;
31
42
  setErrorHandler(handler: Function, prefix?: string): any;
32
- setNotFoundHandler(handler: Function, prefix?: string): any;
43
+ setNotFoundHandler(handler: Function, prefix?: string, excludedRoutes?: RouteInfo[]): any;
33
44
  isHeadersSent(response: any): boolean;
34
45
  getHeader(response: any, name: string): any;
35
46
  setHeader(response: any, name: string, value: string): any;
@@ -4,10 +4,18 @@ import express from 'express';
4
4
  import * as http from 'http';
5
5
  import * as https from 'https';
6
6
  import { pathToRegexp } from 'path-to-regexp';
7
+ import { finished } from 'stream';
7
8
  import { getBodyParserOptions } from './utils/get-body-parser-options.util.js';
9
+ import { getMediaTypeVersion } from './utils/get-media-type-version.util.js';
8
10
  import { addLeadingSlash, isFunction, isNil, isObject, isString, isUndefined, stripEndSlash, } from '@nestjs/common/internal';
9
11
  import { AbstractHttpAdapter } from '@nestjs/core';
10
12
  import { RouterMethodFactory, LegacyRouteConverter, } from '@nestjs/core/internal';
13
+ // Covers "application/json" and the "+json" structured syntax suffix (RFC 6839),
14
+ // e.g. "application/problem+json", with or without parameters.
15
+ function isJsonContentType(contentType) {
16
+ const mediaType = contentType.split(';')[0].trim().toLowerCase();
17
+ return (mediaType.startsWith('application/json') || mediaType.endsWith('+json'));
18
+ }
11
19
  /**
12
20
  * @publicApi
13
21
  */
@@ -23,21 +31,59 @@ export class ExpressAdapter extends AbstractHttpAdapter {
23
31
  super(instance || express());
24
32
  this.instance.use((req, res, next) => {
25
33
  if (this.onResponseHook) {
26
- res.on('finish', () => {
27
- void this.onResponseHook.apply(this, [req, res]);
34
+ res.on('finish', async () => {
35
+ try {
36
+ await this.onResponseHook.apply(this, [req, res]);
37
+ }
38
+ catch (err) {
39
+ this.logger.error(err);
40
+ }
28
41
  });
29
42
  }
30
43
  if (this.onRequestHook) {
31
- void this.onRequestHook.apply(this, [req, res, next]);
44
+ this.runRequestHook(req, res, next);
32
45
  }
33
46
  else {
34
47
  next();
35
48
  }
36
49
  });
37
50
  }
51
+ runRequestHook(req, res, next) {
52
+ let completed = false;
53
+ const doneOnce = (err) => {
54
+ if (completed) {
55
+ if (err) {
56
+ this.logger.error(err);
57
+ }
58
+ return;
59
+ }
60
+ completed = true;
61
+ next(err);
62
+ };
63
+ try {
64
+ // Not returned to Express: rejections share the once-guard with done()
65
+ const result = this.onRequestHook.call(this, req, res, doneOnce);
66
+ if (result && typeof result.then === 'function') {
67
+ result.then(undefined, err => doneOnce(err || new Error('Request hook failed')));
68
+ }
69
+ }
70
+ catch (err) {
71
+ doneOnce(err || new Error('Request hook failed'));
72
+ }
73
+ }
74
+ /**
75
+ * Registers a request hook. Call `done()` to continue processing the request;
76
+ * resolving the returned promise does not continue it. Only the first
77
+ * completion is forwarded to Express. Errors after `done()` are logged
78
+ * without advancing the request again.
79
+ */
38
80
  setOnRequestHook(onRequestHook) {
39
81
  this.onRequestHook = onRequestHook;
40
82
  }
83
+ /**
84
+ * Registers a hook to run after the response has been sent.
85
+ * Thrown errors and rejected promises are logged.
86
+ */
41
87
  setOnResponseHook(onResponseHook) {
42
88
  this.onResponseHook = onResponseHook;
43
89
  }
@@ -54,13 +100,20 @@ export class ExpressAdapter extends AbstractHttpAdapter {
54
100
  stream.once('error', err => {
55
101
  body.errorHandler(err, response);
56
102
  });
103
+ // pipe() leaves the source open when the client disconnects early,
104
+ // also before reply() runs ("close" has already fired by then)
105
+ finished(response, () => {
106
+ if (!stream.readableEnded) {
107
+ stream.destroy();
108
+ }
109
+ });
57
110
  return stream
58
111
  .pipe(response)
59
112
  .on('error', (err) => body.errorLogger(err));
60
113
  }
61
114
  const responseContentType = response.getHeader('Content-Type');
62
115
  if (typeof responseContentType === 'string' &&
63
- !responseContentType.startsWith('application/json') &&
116
+ !isJsonContentType(responseContentType) &&
64
117
  body?.statusCode >= HttpStatus.BAD_REQUEST) {
65
118
  this.logger.warn("Content-Type doesn't match Reply body, you might need a custom ExceptionFilter for non-JSON responses");
66
119
  response.setHeader('Content-Type', 'application/json');
@@ -91,13 +144,23 @@ export class ExpressAdapter extends AbstractHttpAdapter {
91
144
  // root-mounted routes) still pass through the exception layer.
92
145
  return this.use(handler);
93
146
  }
94
- setNotFoundHandler(handler, prefix) {
147
+ setNotFoundHandler(handler, prefix, excludedRoutes) {
95
148
  const normalizedPrefix = this.normalizePrefix(prefix);
96
149
  if (normalizedPrefix) {
97
150
  this.registeredPrefixes.add(normalizedPrefix);
98
151
  const router = express.Router();
99
152
  router.all('*path', handler);
100
- return this.use(normalizedPrefix, router);
153
+ this.use(normalizedPrefix, router);
154
+ // Excluded routes live at the root, out of the prefix router's reach.
155
+ // Mount the handler on each exact path, for every method, so a method
156
+ // miss there (`POST /hello` with only `GET /hello`) still reaches the
157
+ // exception layer. Not `use(path, router)`: a subtree mount would claim
158
+ // a raw `/hello/live` added after `init()`; a wildcard exclusion covers
159
+ // its subtree through its own pattern.
160
+ for (const route of excludedRoutes ?? []) {
161
+ this.instance.all(addLeadingSlash(route.path), handler);
162
+ }
163
+ return;
101
164
  }
102
165
  return this.use((req, res, next) => {
103
166
  // When multiple apps share this adapter, a non-prefixed app's 404
@@ -195,10 +258,22 @@ export class ExpressAdapter extends AbstractHttpAdapter {
195
258
  createMiddlewareFactory(requestMethod) {
196
259
  return (path, callback) => {
197
260
  try {
198
- const convertedPath = LegacyRouteConverter.tryConvert(path);
199
- return this.routerMethodFactory
200
- .get(this.instance, requestMethod)
201
- .call(this.instance, convertedPath, callback);
261
+ // The core marks an exact-match path with a trailing "$" (e.g. "/api$").
262
+ // Express 5 reads "$" literally and "use()" matches by prefix, so strip
263
+ // the marker and register the path as an exact "all()" route instead.
264
+ const isExactPath = path.endsWith('$');
265
+ const convertedPath = LegacyRouteConverter.tryConvert(isExactPath ? path.slice(0, -1) : path);
266
+ let router = this.routerMethodFactory.get(this.instance, requestMethod);
267
+ if (isExactPath && router === this.instance.use) {
268
+ router = this.instance.all;
269
+ }
270
+ // Express routes are not strict, so "/api" also matches "/api/". That
271
+ // path is left to the wildcard entry registered next to it (an optional
272
+ // "{*path}" matches it), so the middleware doesn't run twice.
273
+ const handler = isExactPath
274
+ ? (req, res, next) => req.path.endsWith('/') ? next() : callback(req, res, next)
275
+ : callback;
276
+ return router.call(this.instance, convertedPath, handler);
202
277
  }
203
278
  catch (e) {
204
279
  if (e instanceof TypeError) {
@@ -308,11 +383,9 @@ export class ExpressAdapter extends AbstractHttpAdapter {
308
383
  const MEDIA_TYPE_HEADER = 'Accept';
309
384
  const acceptHeaderValue = req.headers?.[MEDIA_TYPE_HEADER] ||
310
385
  req.headers?.[MEDIA_TYPE_HEADER.toLowerCase()];
311
- const acceptHeaderVersionParameter = acceptHeaderValue
312
- ? acceptHeaderValue.split(';')[1]
313
- : undefined;
386
+ const headerVersion = getMediaTypeVersion(acceptHeaderValue, versioningOptions.key);
314
387
  // No version was supplied
315
- if (isUndefined(acceptHeaderVersionParameter)) {
388
+ if (isUndefined(headerVersion)) {
316
389
  if (Array.isArray(version)) {
317
390
  if (version.includes(VERSION_NEUTRAL)) {
318
391
  return handler(req, res, next);
@@ -320,7 +393,6 @@ export class ExpressAdapter extends AbstractHttpAdapter {
320
393
  }
321
394
  }
322
395
  else {
323
- const headerVersion = acceptHeaderVersionParameter.split(versioningOptions.key)[1];
324
396
  if (Array.isArray(version)) {
325
397
  if (version.includes(headerVersion)) {
326
398
  return handler(req, res, next);
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Returns the version carried by an `Accept` header for media type versioning,
3
+ * e.g. `2` for `application/json;v=2` with the `v=` key.
4
+ *
5
+ * The version parameter may follow other parameters and may belong to any
6
+ * media range of a list (`text/html, application/json;v=2`); the first match
7
+ * wins. Returns `undefined` when no parameter starts with the key.
8
+ *
9
+ * Duplicated in each platform package so that a platform package never
10
+ * depends on a newer `@nestjs/core` than the one installed.
11
+ */
12
+ export declare function getMediaTypeVersion(acceptHeader: string | string[] | undefined, key: string): string | undefined;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Returns the version carried by an `Accept` header for media type versioning,
3
+ * e.g. `2` for `application/json;v=2` with the `v=` key.
4
+ *
5
+ * The version parameter may follow other parameters and may belong to any
6
+ * media range of a list (`text/html, application/json;v=2`); the first match
7
+ * wins. Returns `undefined` when no parameter starts with the key.
8
+ *
9
+ * Duplicated in each platform package so that a platform package never
10
+ * depends on a newer `@nestjs/core` than the one installed.
11
+ */
12
+ export function getMediaTypeVersion(acceptHeader, key) {
13
+ const mediaRanges = []
14
+ .concat(acceptHeader ?? [])
15
+ .flatMap(value => value.split(','));
16
+ for (const mediaRange of mediaRanges) {
17
+ const [, ...parameters] = mediaRange.split(';');
18
+ for (const parameter of parameters) {
19
+ const trimmedParameter = parameter.trim();
20
+ if (trimmedParameter.startsWith(key)) {
21
+ return trimmedParameter.slice(key.length);
22
+ }
23
+ }
24
+ }
25
+ return undefined;
26
+ }
@@ -16,6 +16,10 @@ export interface MulterLimits {
16
16
  parts?: number;
17
17
  /** For multipart forms, the max number of header key=> value pairs to parse Default: 2000(same as node's http). */
18
18
  headerPairs?: number;
19
+ /** Max nesting depth of field names; `a[b][c]` has 2 levels (Default: Infinity) */
20
+ fieldNestingDepth?: number;
21
+ /** Max numeric array index accepted in field names; `a[3]` uses index 3 (Default: Infinity) */
22
+ fieldArrayIndexLimit?: number;
19
23
  }
20
24
  /**
21
25
  * @see https://github.com/expressjs/multer
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nestjs/platform-express",
3
- "version": "12.1.1",
3
+ "version": "12.1.3",
4
4
  "description": "Nest - modern, fast, powerful node.js web framework (@platform-express)",
5
5
  "author": "Kamil Mysliwiec",
6
6
  "license": "MIT",
@@ -32,12 +32,12 @@
32
32
  "tslib": "2.8.1"
33
33
  },
34
34
  "devDependencies": {
35
- "@nestjs/common": "12.1.1",
36
- "@nestjs/core": "12.1.1"
35
+ "@nestjs/common": "12.1.3",
36
+ "@nestjs/core": "12.1.3"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@nestjs/common": "^12.0.0",
40
40
  "@nestjs/core": "^12.0.0"
41
41
  },
42
- "gitHead": "9feedffde8ef8c86f0cd521153740575dc579d88"
42
+ "gitHead": "4514623279f6087808e3c8f08fbf9f37d3c17482"
43
43
  }