mswjs / interceptors

Low-level network interception library.
https://npm.im/@mswjs/interceptors
MIT License
565 stars 127 forks source link

controller.errorWith ends up in the response path if the error is not Error instance #625

Open mikicho opened 2 months ago

mikicho commented 2 months ago

interceptors expects the error to be from type Error in handleRequest, which is not required by Node.

const http = require('http')
const { ClientRequestInterceptor } = require('@mswjs/interceptors/ClientRequest')

const interceptor = new ClientRequestInterceptor()

interceptor.on('request', async function rootListener({ controller }) {
  controller.errorWith({ message: 'custom error', code: 'test' })
})
// interceptor.apply()

const req = http.request('http://example.com')

req.on('response', () => {
  console.log('response');
})
req.on('error', e => {
  console.log('error', e);
})

req.end()

If the interceptor is applied, the program prints response and throws:

/.../nock/node_modules/@mswjs/interceptors/lib/node/chunk-WTJL7BRV.js:234
if (!Reflect.has(headers, kRawHeaders)) {
               ^

TypeError: Reflect.has called on non-object
    at Reflect.has (<anonymous>)
    at getRawFetchHeaders (/home/michael/projects/js/nock/node_modules/@mswjs/interceptors/lib/node/chunk-WTJL7BRV.js:234:16)
    at MockHttpSocket.respondWith (/home/michael/projects/js/nock/node_modules/@mswjs/interceptors/lib/node/chunk-WTJL7BRV.js:488:32)
    at Object.onResponse (/home/michael/projects/js/nock/node_modules/@mswjs/interceptors/lib/node/chunk-WTJL7BRV.js:901:18)
    at handleResponse (/home/michael/projects/js/nock/node_modules/@mswjs/interceptors/lib/node/chunk-TQD7SQGP.js:100:21)
    at handleRequest (/home/michael/projects/js/nock/node_modules/@mswjs/interceptors/lib/node/chunk-TQD7SQGP.js:186:12)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async _ClientRequestInterceptor.onRequest (/home/michael/projects/js/nock/node_modules/@mswjs/interceptors/lib/node/chunk-WTJL7BRV.js:895:32)

Node.js v20.17.0

For real requests, it is called the error callback.

WDYT?

marikaner commented 1 month ago

I have the same issue currently when using @mswjs/interceptors through nock. I am currently unable to produce a reproducible example, so it is very difficult to investigate this. Does anyone have an idea for a workaround until this is fixed? @mikicho maybe?

mikicho commented 1 month ago

@marikaner In order to keep this issue clean, I opened an issue in nock to track this and discuss about workaround (https://github.com/nock/nock/issues/2789)

kettanaito commented 4 weeks ago

I think it should be safe to allow arbitrary error reasons. .destroy() isn't strict about this, as you've mentioned, and some other clients don't support custom error reasons at all.

The only thing, I'd be careful with the handleRequest as it tackles a bunch of scenarios and some of them are not that apparent. But all are covered with tests so any change will be verified.

I'd probably suggest to refactor handleRequest to have two internal callbacks: onResolve and onReject that are called whenever something is returned via .respondWith() or something throws during the request resolution (no matter if via throw or .errorWith()). Basically, to streamline the request handling to two paths:

The current handleResponse and handleResponseError are too vague. Besides, there are situations when thrown errors are responses and get handled as response, which is confusing to read:

https://github.com/mswjs/interceptors/blob/472bcbe4b6b7f0652da10eb647a3aaa898ad4f7c/src/utils/handleRequest.ts#L74-L77

We can likely keep the existing handleResponse/handleResponseError, maybe rename them, and use them within the newly added onResolve and onReject callbacks.

@mikicho, would you like to give this one a try?

mikicho commented 3 weeks ago

Sure! I'd love to contribute to this one... I'm looking into it.

mikicho commented 3 weeks ago

@kettanaito I need clarification on what you have in mind. How is it different from what we currently have?

export async function handleRequest(options) {
  const handleResponse..
  const handleResomposeError..
  ...
  const result = await until...
 if (result.error)... // a.k.a onReject
 if (result.data)... // a.ka onResolve
}
kettanaito commented 3 weeks ago

Right now we have cases where rejections can be translated to successful response handling:

https://github.com/mswjs/interceptors/blob/7e62e17cb5c0d96afcd10aa5420cac78892adff9/src/utils/handleRequest.ts#L76

And successful responses translated to errors:

https://github.com/mswjs/interceptors/blob/7e62e17cb5c0d96afcd10aa5420cac78892adff9/src/utils/handleRequest.ts#L47-L53

This all is intended but reading it is a bit confusing. So I proposed to have higher-level onResolve and onReject callbacks within the handleRequest function to make it easier to read. It's much easier to understand "onResolve triggered with an error, so treat it as an error" than "successful response callback triggered with an error, so it's an error". Do you see my line of thinking?