@nestjs/platform-fastify 12.1.2 → 12.2.0

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.
@@ -6,6 +6,10 @@
6
6
  * media range of a list (`text/html, application/json;v=2`); the first match
7
7
  * wins. Returns `undefined` when no parameter starts with the key.
8
8
  *
9
+ * Quoted parameter values are unescaped, and separators inside them are ignored.
10
+ * An unterminated quoted string runs to the end of the header value, so a
11
+ * version that follows its opening quote is not read.
12
+ *
9
13
  * Duplicated in each platform package so that a platform package never
10
14
  * depends on a newer `@nestjs/core` than the one installed.
11
15
  */
@@ -1,3 +1,29 @@
1
+ /**
2
+ * Splits a header value at separators outside quoted strings in linear time.
3
+ * Backslash-escaped characters inside quoted strings are treated as literals.
4
+ */
5
+ const splitOutsideQuotes = (value, separator) => {
6
+ const parts = [];
7
+ let start = 0;
8
+ let isQuoted = false;
9
+ const len = value.length;
10
+ for (let index = 0; index < len; index++) {
11
+ const character = value[index];
12
+ if (isQuoted && character === '\\') {
13
+ // A quoted-pair consumes the next character, including an escaped quote.
14
+ index++;
15
+ }
16
+ else if (character === '"') {
17
+ isQuoted = !isQuoted;
18
+ }
19
+ else if (character === separator && !isQuoted) {
20
+ parts.push(value.slice(start, index));
21
+ start = index + 1;
22
+ }
23
+ }
24
+ parts.push(value.slice(start));
25
+ return parts;
26
+ };
1
27
  /**
2
28
  * Returns the version carried by an `Accept` header for media type versioning,
3
29
  * e.g. `2` for `application/json;v=2` with the `v=` key.
@@ -6,19 +32,27 @@
6
32
  * media range of a list (`text/html, application/json;v=2`); the first match
7
33
  * wins. Returns `undefined` when no parameter starts with the key.
8
34
  *
35
+ * Quoted parameter values are unescaped, and separators inside them are ignored.
36
+ * An unterminated quoted string runs to the end of the header value, so a
37
+ * version that follows its opening quote is not read.
38
+ *
9
39
  * Duplicated in each platform package so that a platform package never
10
40
  * depends on a newer `@nestjs/core` than the one installed.
11
41
  */
12
42
  export function getMediaTypeVersion(acceptHeader, key) {
13
43
  const mediaRanges = []
14
44
  .concat(acceptHeader ?? [])
15
- .flatMap(value => value.split(','));
45
+ .flatMap(value => splitOutsideQuotes(value, ','));
16
46
  for (const mediaRange of mediaRanges) {
17
- const [, ...parameters] = mediaRange.split(';');
47
+ const [, ...parameters] = splitOutsideQuotes(mediaRange, ';');
18
48
  for (const parameter of parameters) {
19
49
  const trimmedParameter = parameter.trim();
20
50
  if (trimmedParameter.startsWith(key)) {
21
- return trimmedParameter.slice(key.length);
51
+ const version = trimmedParameter.slice(key.length);
52
+ if (version.startsWith('"') && version.endsWith('"')) {
53
+ return version.slice(1, -1).replace(/\\([\s\S])/g, '$1');
54
+ }
55
+ return version;
22
56
  }
23
57
  }
24
58
  }
@@ -12,11 +12,11 @@ export interface MultipartStorageEngine {
12
12
  /**
13
13
  * Same keys as platform-express's `MulterLimits`.
14
14
  *
15
- * Limits that are not set fall back to the options `@fastify/multipart` was
16
- * registered with, and then to its own defaults. Unlike multer (which
17
- * defaults `fileSize` and `parts` to `Infinity`), `@fastify/multipart` caps
18
- * `fileSize` at the Fastify instance's `bodyLimit` (1 MiB unless configured)
19
- * and `parts` at 1000.
15
+ * Limits that are not set, or set to `null` or `undefined`, fall back to the
16
+ * options `@fastify/multipart` was registered with, and then to its own
17
+ * defaults. Unlike multer (which defaults `fileSize` and `parts` to
18
+ * `Infinity`), `@fastify/multipart` caps `fileSize` at the Fastify
19
+ * instance's `bodyLimit` (1 MiB unless configured) and `parts` at 1000.
20
20
  *
21
21
  * @publicApi
22
22
  */
@@ -39,6 +39,20 @@ export interface MultipartLimits {
39
39
  parts?: number;
40
40
  /** For multipart forms, the max number of header key => value pairs to parse (Default: 2000) */
41
41
  headerPairs?: number;
42
+ /**
43
+ * Max nesting depth of field names; `a[b][c]` has 2 levels (Default: 32,
44
+ * where multer's own default is Infinity). Set it to `Infinity` to lift
45
+ * the limit. Like `fieldNameSize`, it does not fall back to the options
46
+ * `@fastify/multipart` was registered with.
47
+ */
48
+ fieldNestingDepth?: number;
49
+ /**
50
+ * Max numeric array index accepted in field names; `a[3]` uses index 3
51
+ * (Default: 1000, where multer's own default is Infinity). Set it to
52
+ * `Infinity` to lift the limit. Like `fieldNameSize`, it does not fall back
53
+ * to the options `@fastify/multipart` was registered with.
54
+ */
55
+ fieldArrayIndexLimit?: number;
42
56
  }
43
57
  /**
44
58
  * Same keys and semantics as platform-express's `MulterOptions`, so option
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Appends a text field to `store` using multer's bracket notation.
3
- * Returns `false` (leaving `store` untouched) for names that would pollute a
4
- * prototype.
3
+ * Returns `false` for names it cannot apply: names that would pollute a
4
+ * prototype (leaving `store` untouched) and appends to an array that already
5
+ * has the maximum length.
5
6
  */
6
7
  export declare function appendField(store: object, key: string, value: unknown): boolean;
@@ -84,11 +84,13 @@ function setValue(ctx, step, current, value) {
84
84
  }
85
85
  obj = Object.create(null);
86
86
  ctx[step.key] = obj;
87
- current.forEach((item, i) => {
88
- if (item !== undefined) {
89
- obj['' + i] = item;
87
+ // Only the indices that hold a value, so that the work depends on the
88
+ // number of items rather than on the length of a sparse array.
89
+ for (const index of Object.keys(current)) {
90
+ if (current[index] !== undefined) {
91
+ obj[index] = current[index];
90
92
  }
91
- });
93
+ }
92
94
  return obj;
93
95
  case 'scalar':
94
96
  obj = Object.create(null);
@@ -99,14 +101,23 @@ function setValue(ctx, step, current, value) {
99
101
  }
100
102
  /**
101
103
  * Appends a text field to `store` using multer's bracket notation.
102
- * Returns `false` (leaving `store` untouched) for names that would pollute a
103
- * prototype.
104
+ * Returns `false` for names it cannot apply: names that would pollute a
105
+ * prototype (leaving `store` untouched) and appends to an array that already
106
+ * has the maximum length.
104
107
  */
105
108
  export function appendField(store, key, value) {
106
109
  const steps = parsePath(key);
107
110
  if (steps.some(step => FORBIDDEN_KEYS.has(String(step.key)))) {
108
111
  return false;
109
112
  }
110
- steps.reduce((ctx, step) => setValue(ctx, step, ctx[step.key], value), store);
113
+ try {
114
+ steps.reduce((ctx, step) => setValue(ctx, step, ctx[step.key], value), store);
115
+ }
116
+ catch (err) {
117
+ if (err instanceof RangeError) {
118
+ return false;
119
+ }
120
+ throw err;
121
+ }
111
122
  return true;
112
123
  }
@@ -1,10 +1,12 @@
1
1
  import { __decorate, __metadata, __param } from "tslib";
2
2
  import { Inject, mixin, Optional, StreamableFile, } from '@nestjs/common';
3
3
  import { HttpAdapterHost } from '@nestjs/core';
4
+ import { isRejectedByPipes } from '@nestjs/core/internal';
4
5
  import { catchError, concatMap, from, throwError } from 'rxjs';
6
+ import { finished } from 'stream';
5
7
  import { MULTIPART_MODULE_OPTIONS } from '../files.constants.js';
6
8
  import { MISSING_PLUGIN_MESSAGE, PLUGIN_NOT_REGISTERED_AT_STARTUP_MESSAGE, WRONG_ADAPTER_MESSAGE, } from './multipart.constants.js';
7
- import { drainRequest, processMultipart, processStream, resolveStorage, validateLimits, } from './multipart.parser.js';
9
+ import { drainRequest, processMultipart, processStream, removeFiles, resolveStorage, validateLimits, } from './multipart.parser.js';
8
10
  import { mergeMultipartOptions } from './multipart.utils.js';
9
11
  const verifiedInstances = new WeakSet();
10
12
  /**
@@ -78,9 +80,23 @@ export function createMultipartInterceptor(spec, localOptions) {
78
80
  this.storage = resolveStorage(this.options);
79
81
  }
80
82
  async intercept(context, next) {
81
- const req = context.switchToHttp().getRequest();
82
- await processMultipart(req, spec, this.options, this.storage);
83
- return next.handle();
83
+ const ctx = context.switchToHttp();
84
+ const req = ctx.getRequest();
85
+ const stored = await processMultipart(req, spec, this.options, this.storage);
86
+ return next.handle().pipe(
87
+ // When the route's pipes reject the request (e.g. `ParseFilePipe`),
88
+ // the route handler never sees the files, and nothing else would
89
+ // remove them. They are removed once the response has been sent, as
90
+ // an exception filter may still use (or remove) them.
91
+ catchError(err => {
92
+ if (isRejectedByPipes(req)) {
93
+ const reply = ctx.getResponse();
94
+ finished(reply.raw, () => {
95
+ removeFiles(this.storage, req, stored).catch(() => undefined);
96
+ });
97
+ }
98
+ return throwError(() => err);
99
+ }));
84
100
  }
85
101
  };
86
102
  MixinInterceptor = __decorate([
@@ -14,9 +14,23 @@ export declare const multerExceptions: {
14
14
  readonly LIMIT_UNEXPECTED_FILE: "Unexpected file field";
15
15
  readonly MISSING_FIELD_NAME: "Field name missing";
16
16
  readonly LIMIT_FIELD_NESTING: "Field name nesting too deep";
17
+ readonly LIMIT_FIELD_ARRAY_INDEX: "Field name array index too large";
17
18
  readonly INVALID_FIELD_NAME: "Invalid field name";
18
19
  };
19
20
  export type MultipartErrorCode = keyof typeof multerExceptions;
21
+ /**
22
+ * Defaults of the field name limits that multer leaves at `Infinity`, which
23
+ * let a single small request build an arbitrarily deep `req.body`
24
+ * (`fieldNestingDepth`) or arrays of arbitrary length (`fieldArrayIndexLimit`).
25
+ * `32` is the nesting depth `body-parser` allows in the urlencoded bodies the
26
+ * ExpressAdapter parses; `1000` is the number of parameters it parses, and
27
+ * the number of parts `@fastify/multipart` accepts. Kept identical to
28
+ * platform-express's `DEFAULT_FIELD_NAME_LIMITS`.
29
+ */
30
+ export declare const DEFAULT_FIELD_NAME_LIMITS: {
31
+ readonly fieldNestingDepth: 32;
32
+ readonly fieldArrayIndexLimit: 1000;
33
+ };
20
34
  /**
21
35
  * The messages platform-express reports for busboy 1.x parser errors
22
36
  * (prefixed with "Multipart: "), which is what multer bundles.
@@ -15,10 +15,24 @@ export const multerExceptions = {
15
15
  LIMIT_UNEXPECTED_FILE: 'Unexpected file field',
16
16
  MISSING_FIELD_NAME: 'Field name missing',
17
17
  LIMIT_FIELD_NESTING: 'Field name nesting too deep',
18
+ LIMIT_FIELD_ARRAY_INDEX: 'Field name array index too large',
18
19
  // A multer 2.4 code that platform-express's table lacks. Raised here for
19
20
  // field names that would pollute a prototype, which multer accepts.
20
21
  INVALID_FIELD_NAME: 'Invalid field name',
21
22
  };
23
+ /**
24
+ * Defaults of the field name limits that multer leaves at `Infinity`, which
25
+ * let a single small request build an arbitrarily deep `req.body`
26
+ * (`fieldNestingDepth`) or arrays of arbitrary length (`fieldArrayIndexLimit`).
27
+ * `32` is the nesting depth `body-parser` allows in the urlencoded bodies the
28
+ * ExpressAdapter parses; `1000` is the number of parameters it parses, and
29
+ * the number of parts `@fastify/multipart` accepts. Kept identical to
30
+ * platform-express's `DEFAULT_FIELD_NAME_LIMITS`.
31
+ */
32
+ export const DEFAULT_FIELD_NAME_LIMITS = {
33
+ fieldNestingDepth: 32,
34
+ fieldArrayIndexLimit: 1000,
35
+ };
22
36
  /**
23
37
  * The messages platform-express reports for busboy 1.x parser errors
24
38
  * (prefixed with "Multipart: "), which is what multer bundles.
@@ -1,6 +1,6 @@
1
1
  import type { FastifyRequest } from 'fastify';
2
2
  import type { IncomingMessage } from 'http';
3
- import type { MultipartField, MultipartLimits, MultipartOptions, MultipartStorageEngine } from '../interfaces/index.js';
3
+ import type { MultipartField, MultipartLimits, MultipartOptions, MultipartStorageEngine, UploadedMultipartFile } from '../interfaces/index.js';
4
4
  /**
5
5
  * Where accepted files end up, named after multer's `FileAppender` strategies.
6
6
  */
@@ -12,6 +12,7 @@ export interface UploadSpec {
12
12
  }
13
13
  /** Same resolution order as multer: `storage`, then `dest`, then memory. */
14
14
  export declare function resolveStorage(options: MultipartOptions): MultipartStorageEngine;
15
+ export declare function removeFiles(storage: MultipartStorageEngine, req: FastifyRequest, files: UploadedMultipartFile[]): Promise<void[]>;
15
16
  /**
16
17
  * Stops parsing and discards the rest of the body, so that an error response
17
18
  * does not race a client that is still uploading (multer does the same
@@ -26,10 +27,11 @@ export declare function drainRequest(raw: IncomingMessage): Promise<void>;
26
27
  export declare function validateLimits(limits: MultipartLimits | undefined): void;
27
28
  /**
28
29
  * Parses the multipart body with multer's semantics and populates
29
- * `req.body` and `req.file` / `req.files`. Requests that are not multipart
30
- * are passed through untouched, as multer does.
30
+ * `req.body` and `req.file` / `req.files`, and returns the stored files.
31
+ * Requests that are not multipart are passed through untouched, as multer
32
+ * does.
31
33
  */
32
- export declare function processMultipart(request: FastifyRequest, spec: UploadSpec, options: MultipartOptions, storage: MultipartStorageEngine): Promise<void>;
34
+ export declare function processMultipart(request: FastifyRequest, spec: UploadSpec, options: MultipartOptions, storage: MultipartStorageEngine): Promise<UploadedMultipartFile[]>;
33
35
  /**
34
36
  * Parses up to and including the first file in `fieldName`, then stops and
35
37
  * hands that file's stream to the route handler (`req.file.stream`). Text
@@ -2,7 +2,7 @@ import { BadRequestException, PayloadTooLargeException } from '@nestjs/common';
2
2
  import { diskStorage } from '../storage/disk.storage.js';
3
3
  import { memoryStorage } from '../storage/memory.storage.js';
4
4
  import { appendField } from './append-field.util.js';
5
- import { busboyExceptions, MISSING_PLUGIN_MESSAGE, multerExceptions, } from './multipart.constants.js';
5
+ import { busboyExceptions, DEFAULT_FIELD_NAME_LIMITS, MISSING_PLUGIN_MESSAGE, multerExceptions, } from './multipart.constants.js';
6
6
  import { MultipartError } from './multipart.error.js';
7
7
  import { transformException } from './multipart.utils.js';
8
8
  /** Same resolution order as multer: `storage`, then `dest`, then memory. */
@@ -22,15 +22,18 @@ function assertPluginRegistered(req) {
22
22
  }
23
23
  /**
24
24
  * Per-route options handed to `req.parts()`; `@fastify/multipart` deep-merges
25
- * them over the options the plugin was registered with. `@fastify/busboy`
26
- * treats `fileSize` and `parts` as "max allowed" (it fires once a limit is
25
+ * them over the options the plugin was registered with. Limits set to `null`
26
+ * or `undefined` are left out, as the merge would copy them over the
27
+ * plugin's limits (and its `fileSize` and `parts` defaults), which
28
+ * `@fastify/busboy` then reads as no limit. `@fastify/busboy` treats
29
+ * `fileSize` and `parts` as "max allowed" (it fires once a limit is
27
30
  * exceeded), so unlike multer there is no `+1` adjustment. Truncation is
28
31
  * detected here (`file.truncated`), so the plugin's own throw is disabled.
29
32
  */
30
33
  function toBusboyConfig(limits, options) {
31
34
  const config = { throwFileSizeLimit: false };
32
35
  if (limits) {
33
- config.limits = { ...limits };
36
+ config.limits = Object.fromEntries(Object.entries(limits).filter(([, value]) => value != null));
34
37
  }
35
38
  if (options.preservePath !== undefined) {
36
39
  config.preservePath = options.preservePath;
@@ -86,9 +89,12 @@ const ABORTED = Symbol('aborted');
86
89
  * the engine (e.g. by a `pipeline()` to a file that cannot be written), which
87
90
  * reports that error itself.
88
91
  *
89
- * Whatever the engine stores after an abort is removed once it calls back,
90
- * as multer does: what it stored when it succeeds, and the file at `path`
91
- * (which engines set before writing, by multer's convention) when it fails.
92
+ * Whatever the engine stores after an abort is removed, as multer does. The
93
+ * file at `path` (which engines set before writing, by multer's convention)
94
+ * is removed right away, as an engine waiting on `pipeline()` never calls
95
+ * back for a stream destroyed after its last chunk; removing it clears
96
+ * `path`. What the engine stored is removed once it calls back: what it
97
+ * returned when it succeeds, and the file at `path` when it fails.
92
98
  */
93
99
  function storeFile(storage, req, file) {
94
100
  return new Promise((resolve, reject) => {
@@ -101,6 +107,10 @@ function storeFile(storage, req, file) {
101
107
  if (!stream.readableEnded && !stream.errored) {
102
108
  aborted = true;
103
109
  resolve(ABORTED);
110
+ const partial = file;
111
+ if (partial.path) {
112
+ removeFiles(storage, req, [partial]).catch(() => undefined);
113
+ }
104
114
  }
105
115
  };
106
116
  stream.once('close', onClose);
@@ -116,7 +126,7 @@ function storeFile(storage, req, file) {
116
126
  });
117
127
  });
118
128
  }
119
- function removeFiles(storage, req, files) {
129
+ export function removeFiles(storage, req, files) {
120
130
  return Promise.all(files.map(file => new Promise(resolve => storage._removeFile(req, file, () => resolve()))));
121
131
  }
122
132
  /**
@@ -197,6 +207,39 @@ function checkFieldNameSize(rawName, limits) {
197
207
  throw new MultipartError('LIMIT_FIELD_KEY');
198
208
  }
199
209
  }
210
+ /**
211
+ * Whether the field name holds a numeric index above `limit`, decided as
212
+ * multer does. Only a name `appendField` parses as a bracket path builds an
213
+ * array; one it keeps as a literal key (`a[6]suffix`, `[6]`) never does.
214
+ */
215
+ function exceedsArrayIndexLimit(name, limit) {
216
+ if (!/^[^[]+(?:\[[^\]]+\])*(?:\[\])?$/.test(name)) {
217
+ return false;
218
+ }
219
+ for (const [, index] of name.matchAll(/\[(\d+)\]/g)) {
220
+ if (Number(index) > limit) {
221
+ return true;
222
+ }
223
+ }
224
+ return false;
225
+ }
226
+ /**
227
+ * `fieldNestingDepth` and `fieldArrayIndexLimit` are multer's own limits
228
+ * (busboy knows neither), so they are checked here, against the decoded name.
229
+ * Unlike multer, they default to `DEFAULT_FIELD_NAME_LIMITS`.
230
+ */
231
+ function checkFieldNameShape(name, limits) {
232
+ const nestingDepth = limits?.fieldNestingDepth ?? DEFAULT_FIELD_NAME_LIMITS.fieldNestingDepth;
233
+ if (name.split('[').length - 1 > nestingDepth) {
234
+ throw new MultipartError('LIMIT_FIELD_NESTING', name);
235
+ }
236
+ const arrayIndexLimit = limits?.fieldArrayIndexLimit ??
237
+ DEFAULT_FIELD_NAME_LIMITS.fieldArrayIndexLimit;
238
+ if (arrayIndexLimit !== Infinity &&
239
+ exceedsArrayIndexLimit(name, arrayIndexLimit)) {
240
+ throw new MultipartError('LIMIT_FIELD_ARRAY_INDEX', name);
241
+ }
242
+ }
200
243
  /**
201
244
  * Adds a text field to `body`, with multer's checks, in multer's order.
202
245
  */
@@ -209,6 +252,7 @@ function appendTextField(body, part, limits) {
209
252
  throw new MultipartError('LIMIT_FIELD_VALUE', part.fieldname);
210
253
  }
211
254
  checkFieldNameSize(rawName, limits);
255
+ checkFieldNameShape(part.fieldname, limits);
212
256
  if (!appendField(body, part.fieldname, part.value)) {
213
257
  throw new MultipartError('INVALID_FIELD_NAME', part.fieldname);
214
258
  }
@@ -229,15 +273,16 @@ function readFilePart(part, limits) {
229
273
  }
230
274
  /**
231
275
  * Parses the multipart body with multer's semantics and populates
232
- * `req.body` and `req.file` / `req.files`. Requests that are not multipart
233
- * are passed through untouched, as multer does.
276
+ * `req.body` and `req.file` / `req.files`, and returns the stored files.
277
+ * Requests that are not multipart are passed through untouched, as multer
278
+ * does.
234
279
  */
235
280
  export async function processMultipart(request, spec, options, storage) {
236
281
  const req = request;
237
282
  assertPluginRegistered(req);
238
283
  resetFiles(req);
239
284
  if (!req.isMultipart()) {
240
- return;
285
+ return [];
241
286
  }
242
287
  const filesLeft = new Map();
243
288
  if (spec.fields !== 'ANY') {
@@ -308,6 +353,7 @@ export async function processMultipart(request, spec, options, storage) {
308
353
  break;
309
354
  }
310
355
  }
356
+ return stored;
311
357
  }
312
358
  /**
313
359
  * Parses up to and including the first file in `fieldName`, then stops and
@@ -13,6 +13,11 @@ export function mergeMultipartOptions(options = {}, localOptions = {}) {
13
13
  if (isPlainObject(options.limits) && isPlainObject(localOptions.limits)) {
14
14
  merged.limits = { ...options.limits, ...localOptions.limits };
15
15
  }
16
+ // `dest` and `storage` both say where files go, and `resolveStorage()`
17
+ // prefers `storage`, so a global storage engine would shadow a local `dest`.
18
+ if (localOptions.dest && !localOptions.storage) {
19
+ delete merged.storage;
20
+ }
16
21
  return merged;
17
22
  }
18
23
  /**
@@ -3,10 +3,24 @@ import { createWriteStream, mkdirSync } from 'fs';
3
3
  import { unlink } from 'fs/promises';
4
4
  import { tmpdir } from 'os';
5
5
  import { join } from 'path';
6
- import { pipeline } from 'stream/promises';
6
+ import { finished, pipeline } from 'stream/promises';
7
7
  function resolveName(resolver, req, file) {
8
8
  return new Promise((resolve, reject) => resolver(req, file, (err, value) => (err ? reject(err) : resolve(value))));
9
9
  }
10
+ const STREAM_DESTROYED_MESSAGE = 'File stream was destroyed';
11
+ /**
12
+ * Rejects once `stream` is destroyed before its end without an error, as
13
+ * `@fastify/multipart` does when the form is cut off. `pipeline()` never
14
+ * settles then if the last chunk had already been pushed: it waits for an
15
+ * 'end' that is never emitted, with the file still open.
16
+ */
17
+ function rejectWhenCutOff(stream) {
18
+ return new Promise((_resolve, reject) => stream.once('close', () => {
19
+ if (!stream.readableEnded && !stream.errored) {
20
+ reject(new Error(STREAM_DESTROYED_MESSAGE));
21
+ }
22
+ }));
23
+ }
10
24
  /**
11
25
  * Writes each file to disk, with multer's `diskStorage` defaults and file
12
26
  * shape (`destination`, `filename`, `path`, `size`).
@@ -30,14 +44,25 @@ export function diskStorage(options = {}) {
30
44
  const store = async (req, file) => {
31
45
  const destination = await resolveName(resolveDestination, req, file);
32
46
  const filename = await resolveName(resolveFilename, req, file);
47
+ // As in multer, no file is created for a stream destroyed in the meantime.
48
+ if (file.stream.destroyed) {
49
+ throw new Error(STREAM_DESTROYED_MESSAGE);
50
+ }
33
51
  const path = join(destination, filename);
34
52
  // As in multer, `path` is known to the file before it is written.
35
53
  file.path = path;
36
54
  const out = createWriteStream(path);
37
55
  try {
38
- await pipeline(file.stream, out);
56
+ await Promise.race([
57
+ pipeline(file.stream, out),
58
+ rejectWhenCutOff(file.stream),
59
+ ]);
39
60
  }
40
61
  catch (err) {
62
+ // Removed once closed: a file still being opened would be created
63
+ // after the unlink.
64
+ out.destroy();
65
+ await finished(out).catch(() => undefined);
41
66
  await unlink(path).catch(() => undefined);
42
67
  throw err;
43
68
  }
@@ -8,12 +8,20 @@ export function memoryStorage() {
8
8
  return {
9
9
  _handleFile(_req, file, callback) {
10
10
  const chunks = [];
11
- file.stream.on('data', (chunk) => chunks.push(chunk));
12
- file.stream.once('error', callback);
13
- file.stream.once('end', () => {
11
+ const onData = (chunk) => chunks.push(chunk);
12
+ const onError = (err) => {
13
+ stopListening();
14
+ callback(err);
15
+ };
16
+ const onEnd = () => {
17
+ stopListening();
14
18
  const buffer = Buffer.concat(chunks);
15
19
  callback(null, { buffer, size: buffer.length });
16
- });
20
+ };
21
+ // `@fastify/multipart` keeps every part's stream until the request
22
+ // ends, and with it these listeners and the chunks they hold.
23
+ const stopListening = () => file.stream.off('data', onData).off('error', onError).off('end', onEnd);
24
+ file.stream.on('data', onData).once('error', onError).once('end', onEnd);
17
25
  },
18
26
  _removeFile(_req, file, callback) {
19
27
  delete file.buffer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nestjs/platform-fastify",
3
- "version": "12.1.2",
3
+ "version": "12.2.0",
4
4
  "description": "Nest - modern, fast, powerful node.js web framework (@platform-fastify)",
5
5
  "author": "Kamil Mysliwiec",
6
6
  "license": "MIT",
@@ -8,6 +8,7 @@
8
8
  "main": "./index.js",
9
9
  "exports": {
10
10
  ".": "./index.js",
11
+ "./package.json": "./package.json",
11
12
  "./multipart": "./multipart/index.js",
12
13
  "./*.js": "./*.js",
13
14
  "./*": "./*.js"
@@ -26,13 +27,13 @@
26
27
  "access": "public"
27
28
  },
28
29
  "dependencies": {
29
- "@fastify/cors": "11.3.0",
30
- "@fastify/formbody": "9.0.0",
31
- "@fastify/middie": "9.3.4",
30
+ "@fastify/cors": "11.3.1",
31
+ "@fastify/formbody": "9.0.1",
32
+ "@fastify/middie": "9.3.5",
32
33
  "fast-querystring": "1.1.2",
33
- "fastify": "5.12.5",
34
- "find-my-way": "9.9.0",
35
- "light-my-request": "6.6.0",
34
+ "fastify": "5.12.7",
35
+ "find-my-way": "9.10.0",
36
+ "light-my-request": "6.8.0",
36
37
  "path-to-regexp": "8.4.2",
37
38
  "tslib": "2.8.1"
38
39
  },
@@ -54,5 +55,5 @@
54
55
  "optional": true
55
56
  }
56
57
  },
57
- "gitHead": "8ab60aa1b2927fbe0e23135ffc65d610c623236a"
58
+ "gitHead": "af37111368de7fd2dfab0bdec2f530c7845510c2"
58
59
  }