Repository: nestjs/nest
Pull request #11667 (2023-05-25 16:46:03)

feat(express): add support for multer.none
Adds an interceptor for multer's none option https://github.com/expressjs/multer#none. This allows for Nest.js users to process `multipart/form-data` that does not include any files.

## PR Checklist
Please check if your PR fulfills the following requirements:

- [x] The commit message follows our guidelines: https://github.com/nestjs/nest/blob/master/CONTRIBUTING.md
- [x] Tests for the changes have been added (for bug fixes / features)
- [x] Docs have been added / updated (for bug fixes / features)


## PR Type
What kind of change does this PR introduce?

<!-- Please check the one that applies to this PR using "x". -->
- [ ] Bugfix
- [x] Feature
- [ ] Code style update (formatting, local variables)
- [ ] Refactoring (no functional changes, no api changes)
- [ ] Build related changes
- [ ] CI related changes
- [ ] Other... Please describe:

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->

Issue Number: N/A

I am implementing a feature that uses the [Sendgrid Inbound Parse API](https://docs.sendgrid.com/for-developers/parsing-email/setting-up-the-inbound-parse-webhook#raw-parameters). This sends the data encoded as `multipart/form-data` however the payload includes no files. I believe all data are sent as strings/text.

I reached for the Nest.js MulterModule but it currently does not support [multer's none option](https://github.com/expressjs/multer#none). `multer.none` parses only the text fields in the body and raises an exception if any files are submitted.

## What is the new behavior?

This PR adds a new NoFilesInterceptor and associated tests. This would allow MulterModule users to accept `multipart/form-data` without any files.

## Does this PR introduce a breaking change?
- [ ] Yes
- [x] No

<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->


## Other information

There is a separate but related PR in the docs project https://github.com/nestjs/docs.nestjs.com/pull/2747.

Also, if there is a way to natively handle text only `multipart/form-data` without the MulterModule would be great to know about that.

coveralls: ## Pull Request Test Coverage Report for [Build a067c295-5272-4472-b860-e85dce373f81](https://coveralls.io/builds/60320026)

* **15** of **17**   **(88.24%)**  changed or added relevant lines in **1** file are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage decreased (**-0.01%**) to **92.624%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [packages/platform-express/multer/interceptors/no-files.interceptor.ts](https://coveralls.io/builds/60320026/source?filename=packages%2Fplatform-express%2Fmulter%2Finterceptors%2Fno-files.interceptor.ts#L45) | 15 | 17 | 88.24%
<!-- | **Total:** | **15** | **17** | **88.24%** | -->


|  Totals | [![Coverage Status](https://coveralls.io/builds/60320026/badge)](https://coveralls.io/builds/60320026) |
| :-- | --: |
| Change from base [Build ca6b0cd6-9a68-4f95-9278-bca8f4b87731](https://coveralls.io/builds/60300524): |  -0.01% |
| Covered Lines: | 6542 |
| Relevant Lines: | 7063 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/packages/platform-express/multer/interceptors/index.ts b/packages/platform-express/multer/interceptors/index.ts
index 5f76d7d47..855620d97 100644
--- a/packages/platform-express/multer/interceptors/index.ts
+++ b/packages/platform-express/multer/interceptors/index.ts
@@ -2,3 +2,4 @@ export * from './any-files.interceptor';
 export * from './file-fields.interceptor';
 export * from './file.interceptor';
 export * from './files.interceptor';
+export * from './no-files.interceptor';
diff --git a/packages/platform-express/multer/interceptors/no-files.interceptor.ts b/packages/platform-express/multer/interceptors/no-files.interceptor.ts
new file mode 100644
index 000000000..1607917d7
--- /dev/null
+++ b/packages/platform-express/multer/interceptors/no-files.interceptor.ts
@@ -0,0 +1,56 @@
+import {
+  CallHandler,
+  ExecutionContext,
+  Inject,
+  mixin,
+  NestInterceptor,
+  Optional,
+  Type,
+} from '@nestjs/common';
+import * as multer from 'multer';
+import { Observable } from 'rxjs';
+import { MULTER_MODULE_OPTIONS } from '../files.constants';
+import { MulterModuleOptions } from '../interfaces';
+import { MulterOptions } from '../interfaces/multer-options.interface';
+import { transformException } from '../multer/multer.utils';
+
+type MulterInstance = any;
+
+export function NoFilesInterceptor(
+  localOptions?: MulterOptions,
+): Type<NestInterceptor> {
+  class MixinInterceptor implements NestInterceptor {
+    protected multer: MulterInstance;
+
+    constructor(
+      @Optional()
+      @Inject(MULTER_MODULE_OPTIONS)
+      options: MulterModuleOptions = {},
+    ) {
+      this.multer = (multer as any)({
+        ...options,
+        ...localOptions,
+      });
+    }
+
+    async intercept(
+      context: ExecutionContext,
+      next: CallHandler,
+    ): Promise<Observable<any>> {
+      const ctx = context.switchToHttp();
+
+      await new Promise<void>((resolve, reject) =>
+        this.multer.none()(ctx.getRequest(), ctx.getResponse(), (err: any) => {
+          if (err) {
+            const error = transformException(err);
+            return reject(error);
+          }
+          resolve();
+        }),
+      );
+      return next.handle();
+    }
+  }
+  const Interceptor = mixin(MixinInterceptor);
+  return Interceptor;
+}


Test patch:
diff --git a/packages/platform-express/test/multer/interceptors/no-files.inteceptor.spec.ts b/packages/platform-express/test/multer/interceptors/no-files.inteceptor.spec.ts
new file mode 100644
index 000000000..5b5289941
--- /dev/null
+++ b/packages/platform-express/test/multer/interceptors/no-files.inteceptor.spec.ts
@@ -0,0 +1,44 @@
+import { CallHandler } from '@nestjs/common';
+import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host';
+import { expect } from 'chai';
+import { of } from 'rxjs';
+import * as sinon from 'sinon';
+import { NoFilesInterceptor } from '../../../multer/interceptors/no-files.interceptor';
+
+describe('NoFilesInterceptor', () => {
+  it('should return metatype with expected structure', async () => {
+    const targetClass = NoFilesInterceptor();
+    expect(targetClass.prototype.intercept).to.not.be.undefined;
+  });
+  describe('intercept', () => {
+    let handler: CallHandler;
+    beforeEach(() => {
+      handler = {
+        handle: () => of('test'),
+      };
+    });
+    it('should call none() with expected params', async () => {
+      const target = new (NoFilesInterceptor())();
+
+      const callback = (req, res, next) => next();
+      const noneSpy = sinon
+        .stub((target as any).multer, 'none')
+        .returns(callback);
+
+      await target.intercept(new ExecutionContextHost([]), handler);
+
+      expect(noneSpy.called).to.be.true;
+    });
+    it('should transform exception', async () => {
+      const target = new (NoFilesInterceptor())();
+      const err = {};
+      const callback = (req, res, next) => next(err);
+      (target as any).multer = {
+        none: () => callback,
+      };
+      (target.intercept(new ExecutionContextHost([]), handler) as any).catch(
+        error => expect(error).to.not.be.undefined,
+      );
+    });
+  });
+});