nestjs / nest

A progressive Node.js framework for building efficient, scalable, and enterprise-grade server-side applications with TypeScript/JavaScript 🚀
https://nestjs.com
MIT License
65.84k stars 7.47k forks source link

Unhandled promise rejection in interceptor #13759

Closed omerd-cyera closed 1 week ago

omerd-cyera commented 1 week ago

Is there an existing issue for this?

Current behavior

Receiving error events in the interceptor observer before all interceptors have been called triggers an unhandled promise rejection. I have attached a codesandbox link that shows this happening. Note that to get this behavior, we have to use something like rxjss merge.

I ran into this issue when creating an interceptor that returns something like merge(next.handle(), interval()), and the interval throws before all the next.handle finish.

Minimum reproduction code

https://codesandbox.io/p/devbox/nestjs-interceptor-unhandled-promise-rejection-forked-lj6l5p?layout=%257B%2522sidebarPanel%2522%253A%2522EXPLORER%2522%252C%2522rootPanelGroup%2522%253A%257B%2522direction%2522%253A%2522horizontal%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522id%2522%253A%2522ROOT_LAYOUT%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522cly769yn800072069cmwkqb57%2522%252C%2522sizes%2522%253A%255B60.578512396694215%252C39.421487603305785%255D%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522EDITOR%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522id%2522%253A%2522cly769yn800022069kvmqvqso%2522%257D%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522SHELLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522id%2522%253A%2522cly769yn800042069iorhj886%2522%257D%255D%252C%2522sizes%2522%253A%255B100%255D%257D%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522DEVTOOLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522id%2522%253A%2522cly769yn8000620690y7nu3vm%2522%257D%255D%252C%2522sizes%2522%253A%255B100%255D%257D%255D%252C%2522sizes%2522%253A%255B50%252C50%255D%257D%252C%2522tabbedPanels%2522%253A%257B%2522cly769yn800022069kvmqvqso%2522%253A%257B%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522cly769yn8000120698e3l0qgb%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522FILE%2522%252C%2522filepath%2522%253A%2522%252FREADME.md%2522%252C%2522state%2522%253A%2522IDLE%2522%257D%252C%257B%2522id%2522%253A%2522cly76ap0n002n2069l9yw0fkz%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522FILE%2522%252C%2522initialSelections%2522%253A%255B%257B%2522startLineNumber%2522%253A11%252C%2522startColumn%2522%253A1%252C%2522endLineNumber%2522%253A11%252C%2522endColumn%2522%253A1%257D%255D%252C%2522filepath%2522%253A%2522%252Fsrc%252Fapp.module.ts%2522%252C%2522state%2522%253A%2522IDLE%2522%257D%252C%257B%2522id%2522%253A%2522cly77c3wx00022069ds6j0qho%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522FILE%2522%252C%2522initialSelections%2522%253A%255B%257B%2522startLineNumber%2522%253A1%252C%2522startColumn%2522%253A1%252C%2522endLineNumber%2522%253A78%252C%2522endColumn%2522%253A1%257D%255D%252C%2522filepath%2522%253A%2522%252Fsrc%252Fapp.controller.ts%2522%252C%2522state%2522%253A%2522IDLE%2522%257D%255D%252C%2522id%2522%253A%2522cly769yn800022069kvmqvqso%2522%252C%2522activeTabId%2522%253A%2522cly77c3wx00022069ds6j0qho%2522%257D%252C%2522cly769yn8000620690y7nu3vm%2522%253A%257B%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522cly769yn800052069k7ueutez%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522TASK_PORT%2522%252C%2522taskId%2522%253A%2522start%2522%252C%2522port%2522%253A3000%252C%2522path%2522%253A%2522%2522%257D%255D%252C%2522id%2522%253A%2522cly769yn8000620690y7nu3vm%2522%252C%2522activeTabId%2522%253A%2522cly769yn800052069k7ueutez%2522%257D%252C%2522cly769yn800042069iorhj886%2522%253A%257B%2522id%2522%253A%2522cly769yn800042069iorhj886%2522%252C%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522cly769yn800032069740iyvsh%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522TASK_LOG%2522%252C%2522taskId%2522%253A%2522start%2522%257D%255D%252C%2522activeTabId%2522%253A%2522cly769yn800032069740iyvsh%2522%257D%257D%252C%2522showDevtools%2522%253Atrue%252C%2522showShells%2522%253Atrue%252C%2522showSidebar%2522%253Atrue%252C%2522sidebarPanelSize%2522%253A15%257D

Steps to reproduce

It should trigger automatically when the integrated browser opens

Expected behavior

I would expect all subsequent exceptions after the first one to be either passed to the exception handling mechanism, or ignored.

Package

Other package

No response

NestJS version

10.3.2

Packages versions

{
  "name": "nest-typescript-starter",
  "private": true,
  "version": "1.0.0",
  "description": "Nest TypeScript starter repository",
  "license": "MIT",
  "scripts": {
    "build": "nest build",
    "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
    "start": "nest start",
    "start:dev": "nest start --watch",
    "start:debug": "nest start --debug --watch",
    "start:prod": "node dist/main",
    "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:cov": "jest --coverage",
    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/jest/bin/jest --runInBand",
    "test:e2e": "jest --config ./test/jest-e2e.json"
  },
  "dependencies": {
    "@nestjs/common": "^10.3.2",
    "@nestjs/core": "^10.3.2",
    "@nestjs/platform-express": "^10.3.2",
    "reflect-metadata": "^0.2.1",
    "rxjs": "^7.8.1"
  },
  "devDependencies": {
    "@nestjs/cli": "^10.3.1",
    "@nestjs/schematics": "^10.1.0",
    "@nestjs/testing": "^10.3.2",
    "@swc/cli": "^0.3.9",
    "@swc/core": "^1.4.0",
    "@types/express": "^4.17.21",
    "@types/jest": "^29.5.12",
    "@types/node": "^20.11.16",
    "@types/supertest": "^6.0.2",
    "@typescript-eslint/eslint-plugin": "^6.21.0",
    "@typescript-eslint/parser": "^6.21.0",
    "eslint": "^8.56.0",
    "eslint-config-prettier": "^9.1.0",
    "eslint-plugin-prettier": "^5.1.3",
    "jest": "^29.7.0",
    "prettier": "^3.2.5",
    "source-map-support": "^0.5.21",
    "supertest": "^6.3.4",
    "ts-jest": "^29.1.2",
    "ts-loader": "^9.5.1",
    "ts-node": "^10.9.2",
    "tsconfig-paths": "^4.2.0",
    "typescript": "^5.3.3"
  },
  "jest": {
    "moduleFileExtensions": [
      "js",
      "json",
      "ts"
    ],
    "rootDir": "src",
    "testRegex": ".*\\.spec\\.ts$",
    "transform": {
      "^.+\\.(t|j)s$": "ts-jest"
    },
    "collectCoverageFrom": [
      "**/*.(t|j)s"
    ],
    "coverageDirectory": "../coverage",
    "testEnvironment": "node"
  }
}

Node.js version

v20.9.0

In which operating systems have you tested?

Other

The package.json and node version I specified are the ones in codesandbox. But it happens to me on node 18.17.0 and 10.3.0 too.

I think I know what the issue is. I managed to resolve it locally by patching packages/core/interceptors/interceptors-consumer.ts:33

I replaced

    const nextFn = async (i = 0) => {
      if (i >= interceptors.length) {
        return defer(AsyncResource.bind(() => this.transformDeferred(next)));
      }
      const handler: CallHandler = {
        handle: () => fromPromise(nextFn(i + 1)).pipe(mergeAll()),
      };
      return interceptors[i].intercept(context, handler);
    };

with

    const nextFn = async (i = 0) => {
      if (i >= interceptors.length) {
        return defer(AsyncResource.bind(() => this.transformDeferred(next)));
      }
      const handler: CallHandler = {
        handle: () => defer(() => from(nextFn(i + 1))).pipe(mergeAll()),
      };
      return interceptors[i].intercept(context, handler);
    };

When looking at this stackOverflow question: https://stackoverflow.com/questions/39319279/convert-promise-to-observable I followed coderrr22's warning on the accepted answer

Please be careful with this solution, Because it may result in an unhandled promise rejection which can break your code! If you are using from operator you must handle your promise rejection in the promise within the from operator(using .catch()), otherwise the promise may execute and throw an error, while its wrapping observable hasn't been subscribed yet, and if your error handling is piped to the observable it will not catch the error ! (I prefer using defer instead and handling the errors centrally from the observable).

kamilmysliwiec commented 1 week ago

Would you like to create a PR for this issue?

omerd-cyera commented 1 week ago

Would you like to create a PR for this issue?

@kamilmysliwiec sure

sheepster1 commented 1 week ago

@kamilmysliwiec Created the PR from another account

kamilmysliwiec commented 1 week ago

Let's track this here https://github.com/nestjs/nest/pull/13762