mswjs / interceptors

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

How to return the rawHeaders? #448

Closed mikicho closed 11 months ago

mikicho commented 11 months ago

The respondWith function expects Response object that (AFAIK) doesn't support rawHeaders, but it seems like it's possible to test rawHeaders in the code

image

kettanaito commented 11 months ago

Yes, you are correct; the Response instance will have the headers normalized per spec (not the part of the library, we're relying on Node.js for that).

Can you tell me more if this is related to any particular use case you're trying to achieve? From the developer's perspective, they should operate with the headers per Fetch API specification, where header names are case-insensitive. That's also the case when working on backend frameworks, like Express. I'd like to go as far and say it's a part of a bigger standard but I'm afraid I don't recall it by heart.

mikicho commented 11 months ago

I'd like to go as far and say it's a part of a bigger standard but I'm afraid I don't recall it by heart.

I agree.

Can you tell me more if this is related to any particular use case you're trying to achieve?

The thing is that the IncomingMessage returns the rawHeaders along with the headers (docs)

We have a test which make sure both headers and rawHeaders are correct.

kettanaito commented 11 months ago

Interceptors don't expose you to the underlying ClientRequest/IncomingMessage instances so you cannot access the rawHeaders. While operating with the library, you work with the Response.headers, which is the correct representation of the response headers.

You can still have that test since you can listen to the response event on the request and have the full reference to the IncomingMessage there to assert rawHeaders and so forth.

// nock.test.js
const req = http.get('/resource')
req.on('response', (res) => {
  // This will be emitted for both mocked and bypassed requests.
  // Afaik, "response" event is emitted once the first byte of the
  // response is received, which is headers.
  expect(res.rawHeaders).toBe('...')
  expect(res.headers).toEqual({ ... })
})

I understand that based on the way the test is constructed, this direct reference to the request instance may not be possible.

I'd recommend not to assert rawHeaders if using abstractions like Interceptors but I don't have enough context in Nock to do so.

kettanaito commented 11 months ago

For more context on the Interceptors' side:

We are propagating the raw headers to this.response.rawHeaders to construct a valid IncomingMessage out of the mocked response. This is for compatibility reasons with the original request surface (i.e. the developer's code). But when using the library, you operate with Fetch API classes alone.

The test you mentioned above should still work because rawHeaders is returned from your request client (got). You can still do that.

mikicho commented 11 months ago

Thanks! I think the problem is that the Response class doesn't have the concept of rawHeaders, so we can't pass whatever we want back to MSW:

interceptor.on('request', (request) => {
  request.respondWith(new Response(200, {headers: { "Cant-be-Capital": "OK" }})
})
kettanaito commented 11 months ago

You can pass whatever you want but header names will be normalized as per the Fetch API specification (not done by us, done by Undici in Node.js and by the browser in the browser). This means you can access the header by both its raw name and the normalized name:

const headers = new Headers()
headers.set('Cant-be-CapItal', 'ok')

headers.get('cant-be-capital') // "ok"
headers.get('CaNt-Be-CaPiTaL') // "ok"

Header name casing doesn't matter when accessing the headers.

In this context, you can assert that the header is available by its raw name even using Response.headers. If you wish to assert the rawHeaders explicitly, you can still do that because you're likely the one constructing the http.ClientRequest() in your test/app and you have access to req.on('response', (res) => res.rawHeaders) that will be present in the correct shape for both mocked and bypassed responses.

mikicho commented 11 months ago

Maybe I'm missing something, but this:

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

const interceptor = new ClientRequestInterceptor({
  name: 'my-interceptor',
})
interceptor.apply();
interceptor.on('request', ({ request }) => {
  request.respondWith(new Response('body', {
    status: 200, headers: {
      'X-User-Agent': 'Test',
    }
  }))
});

(async function () {
  const { rawHeaders } = await got('http://example.test/foo')
  console.log(rawHeaders)
})()

Prints:

[ 'content-type', 'text/plain;charset=UTF-8', 'x-user-agent', 'Test' ]

the user agent isn't the "raw header". AFAIK, Node.js sends the rawHeaders as-is

kettanaito commented 11 months ago

Released: v0.25.7 🎉

This has been released in v0.25.7!

Make sure to always update to the latest version (npm i @mswjs/interceptors@latest) to get the newest features and bug fixes.


Predictable release automation by @ossjs/release.