Skip to content

fix(core): subscribe to handler observables from other rxjs copies - #18215

Merged
kamilmysliwiec merged 1 commit into
nestjs:masterfrom
koreahghg:fix/interceptor-foreign-observables
Oct 21, 2026
Merged

kamilmysliwiec merged 1 commit into
nestjs:masterfrom
koreahghg:fix/interceptor-foreign-observables

Conversation

@koreahghg

Copy link
Copy Markdown
Contributor

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • 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?

InterceptorsConsumer.transformDeferred() decides whether a handler's result is a stream with res instanceof Observable, which only matches the copy of rxjs that @nestjs/core loads. When a handler returns an Observable created by another copy of rxjs (e.g. one bundled by a client library, or a duplicated rxjs in a monorepo), the check fails and the result is wrapped with Promise.resolve(res). The Observable is never subscribed: interceptors receive the Observable object itself in next.handle(), and it is sent as the response (serialized as {}).

The same handler works without interceptors, because RouterExecutionContext / RouterResponseController already use isObservable(), so adding any interceptor (even a pass-through, or a global ClassSerializerInterceptor) changes the response.

This is the same class of issue that fec478e fixed for guards in GuardsConsumer.

Issue Number: N/A

What is the new behavior?

transformDeferred() checks the result with isObservable(), as GuardsConsumer already does, so Observables from other copies of rxjs are subscribed and the values they emit are passed through the interceptor chain. Added a regression test to interceptors-consumer.spec.ts (fails before the change, passes after).

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

packages/core, packages/microservices and packages/websockets unit tests pass except 4 tests (router-exception-filters, client-kafka, server-redis) that fail identically on current master without this change.

InterceptorsConsumer recognised an Observable returned by a route handler
only with "instanceof Observable", against the copy of rxjs @nestjs/core
loads. An Observable from another copy, such as one a client library
brings along, was never subscribed to: the Observable object itself was
emitted to the interceptors and sent as the response, while the same
handler worked without interceptors.

The result is now checked with isObservable(), as GuardsConsumer already
does, so the values such an Observable emits are passed on.
@kamilmysliwiec
kamilmysliwiec merged commit 5d71974 into nestjs:master Oct 21, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants