mirror of
https://github.com/nestjs/nest.git
synced 2026-10-28 09:45:43 +08:00
fix(express,fastify): let a route's dest replace the module's storage
mergeMulterOptions() and mergeMultipartOptions() spread the route's options over the module's, so a module-level storage and a route-level dest both survived the merge. multer, like resolveStorage() on Fastify, takes storage first, so the route's dest was silently ignored. A route that sets dest but no storage now drops the module's storage. With a module-level memoryStorage(), a route that sets dest now writes files to disk, so file.buffer is undefined and file.path is set. With a module-level disk, S3 or other engine, that route's files now go to its dest instead. Discovered by Anthropic's OSS Scanner, as bug ANT-2026-X70NB90Z.
This commit is contained in:
4 files changed
+35
No files matched your search
@@ -33,6 +33,11 @@ export function mergeMulterOptions(
|
||||
if (isPlainObject(options.limits) && isPlainObject(localOptions.limits)) {
|
||||
merged.limits = { ...options.limits, ...localOptions.limits };
|
||||
}
|
||||
// `dest` and `storage` both say where files go, and multer prefers
|
||||
// `storage`, so a global storage engine would shadow a local `dest`.
|
||||
if (localOptions.dest && !localOptions.storage) {
|
||||
delete merged.storage;
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,18 @@ describe('mergeMulterOptions', () => {
|
||||
const merged = mergeMulterOptions({ dest: '/global' }, { dest: '/local' });
|
||||
expect(merged.dest).toBe('/local');
|
||||
});
|
||||
it('should let a local "dest" override a global "storage"', () => {
|
||||
const merged = mergeMulterOptions(
|
||||
{ storage: multer.memoryStorage() },
|
||||
{ dest: '/local' },
|
||||
);
|
||||
expect(merged).toEqual({ dest: '/local' });
|
||||
});
|
||||
it('should keep a global "storage" when the route sets no "dest"', () => {
|
||||
const storage = multer.memoryStorage();
|
||||
const merged = mergeMulterOptions({ storage }, { preservePath: true });
|
||||
expect(merged.storage).toBe(storage);
|
||||
});
|
||||
it('should merge limits key-by-key instead of replacing the whole object', () => {
|
||||
const merged = mergeMulterOptions(
|
||||
{ limits: { files: 2, fieldNameSize: 100 } },
|
||||
|
||||
@@ -30,6 +30,11 @@ export function mergeMultipartOptions(
|
||||
if (isPlainObject(options.limits) && isPlainObject(localOptions.limits)) {
|
||||
merged.limits = { ...options.limits, ...localOptions.limits };
|
||||
}
|
||||
// `dest` and `storage` both say where files go, and `resolveStorage()`
|
||||
// prefers `storage`, so a global storage engine would shadow a local `dest`.
|
||||
if (localOptions.dest && !localOptions.storage) {
|
||||
delete merged.storage;
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
mergeMultipartOptions,
|
||||
transformException,
|
||||
} from '../../../multipart/multipart/multipart.utils.js';
|
||||
import { memoryStorage } from '../../../multipart/storage/index.js';
|
||||
|
||||
describe('mergeMultipartOptions', () => {
|
||||
it('should let local options override global options', () => {
|
||||
@@ -17,6 +18,18 @@ describe('mergeMultipartOptions', () => {
|
||||
).toEqual({ dest: '/local' });
|
||||
});
|
||||
|
||||
it('should let a local "dest" override a global "storage"', () => {
|
||||
expect(
|
||||
mergeMultipartOptions({ storage: memoryStorage() }, { dest: '/local' }),
|
||||
).toEqual({ dest: '/local' });
|
||||
});
|
||||
|
||||
it('should keep a global "storage" when the route sets no "dest"', () => {
|
||||
const storage = memoryStorage();
|
||||
const merged = mergeMultipartOptions({ storage }, { preservePath: true });
|
||||
expect(merged.storage).toBe(storage);
|
||||
});
|
||||
|
||||
it('should merge limits key-by-key', () => {
|
||||
const merged = mergeMultipartOptions(
|
||||
{ limits: { files: 2, fileSize: 1 } },
|
||||
|
||||
Reference in new issue
Block a user