-
-
Notifications
You must be signed in to change notification settings - Fork 127
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(ClientRequest): support "rawHeaders" in Fetch API Headers #598
Conversation
Hi, @mikicho. Can you please tell me more about this use case? We are effectively testing how Fetch API Interceptors construct a correct Response instance. Maybe there's something else we should be testing on the |
@kettanaito IIRC, the current response differs from what Node.js returns with |
Got it. It's a bit complicated. let h = new Headers()
h.append('X-Custom', 'one')
h.append('x-custom', 'one') This instance will only store the raw name as For us to prevent that, we need to spy on |
@kettanaito, I put some thought into this a few months ago.
|
I don't mind trying to breach the gap here but I see a problem: Node.js itself lowecases the header names before setting them: Even if I record What you are proposing means a completely new API just to set |
@mikicho, can you please show me the original Nock test behind this issue? If you interface with |
Okay, I found how one can set raw headers: using |
https://github.com/nock/nock/blob/ca9e61674a5e31086dd766f1f2d917b21ae4b358/tests/got/test_reply_headers.js#L58 Also, I do some work around headers when creating the
Nock returns |
The main difficult here is that in Fetch API, this is one header: headers.append('X-Custom-Header', 'one')
headers.append('x-custom-header', 'two') While in
No matter what solution we ship, it will be incomplete due to the design of the Fetch API headers class. Moreover, headers behavior across Node.js versions differs. The tests we have now pass on v18.19, fail on v18.20. Since by the spec all headers are lowecased, none of those changes are treated as breaking. Edit: I can confirm that since v18.19, Node.js doesn't even keep the map of raw header names anymore. The header names are lowercased as soon as they enter Here's a new Headers({ 'X-Custom-Header': 'yes' })
HeadersList {
cookies: null,
[Symbol(headers map)]: Map(1) {
'x-custom-header' => { name: 'x-custom-header', value: 'Yes' }
},
[Symbol(headers map sorted)]: [ [ 'x-custom-header', 'Yes' ] ]
} |
41ac23d
to
d2c93a0
Compare
d2c93a0
to
6800c35
Compare
This is an annoying gap, indeed. This is why I think we should create a separate flow between these two, if possible. |
@mikicho, I somewhat siding with the fetch api though. Header fields are case-insensitive per spec. Anyhow, I managed to record the raw headers with a bit of transparent proxying over the |
@kettanaito Thank you for working on this!
AFAIK
|
@mikicho, makes sense. I've pushed the last patch that should give us consistent raw headers recording and have the existing tests passing. Could you please give this PR a try before we merge this? I wonder how it complies with Nock. Thanks. |
*/ | ||
serverResponse.writeHead( | ||
response.status, | ||
response.statusText || STATUS_CODES[response.status], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We've deleted this prematurely. Probably cached test results but this was failing. We guarantee to set a response status code even if none was explicitly provided. This isn't what Fetch API does but we do this.
@@ -535,7 +541,7 @@ export class MockHttpSocket extends MockSocket { | |||
|
|||
// Similarly, create a new stream for each response. | |||
if (canHaveBody) { | |||
this.responseStream = new Readable() | |||
this.responseStream = new Readable({ read() {} }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was also erroring. Likely an oversight. read()
must always be implemented.
// Spy on `Header.prototype.set` and `Header.prototype.append` calls | ||
// and record the raw header names provided. This is to support | ||
// `IncomingMessage.prototype.rawHeaders`. | ||
recordRawFetchHeaders() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm enabling the raw headers recording as a part of the ClientRequest interceptor. This is the only place we need it for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about unmocked requests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The behavior should be the same for bypassed requests. Let me check tomorrow if I have tests to prove that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kettanaito Seems like it is working 🎉
I added a test. If you meant something else, feel free to remove this.
Also, I updated the minimum node version in the package.json to 18.20.0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kettanaito For some reason, I get this error:
Exception has occurred: TypeError: Cannot assign to read only property 'Symbol(kRawHeaders)' of object '#<_Headers>'
at Object.construct (/..../mswjs-interceptors/lib/node/chunk-CFRXZJO4.js:192:39)
at createResponse (/..../nock/lib/create_response.js:43:20)
at OverriddenClientRequest. (/.../nock/lib/intercept.js:405:28)
at OverriddenClientRequest.emit (node:events:530:35)
at Timeout.respond [as _onTimeout] (/.../nock/lib/playback_interceptor.js:307:11)
at listOnTimeout (node:internal/timers:573:17)
at process.processTimers (node:internal/timers:514:7)
I'm wondering if I do something wrong:
function createResponse(message: IncomingMessage) {
...
const rawHeaders = new Headers()
for (let i = 0; i < message.rawHeaders.length; i += 2) {
rawHeaders.append(message.rawHeaders[i], message.rawHeaders[i + 1])
}
const response = new Response(responseBodyOrNull, {
status: message.statusCode,
statusText: message.statusMessage || STATUS_CODES[message.statusCode],
headers: rawHeaders,
})
return response
}
The error is thrown from this line.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The errors disappear when I set writable: true
for kRawHeaders
.
I'm wondering why it works for interceptors
tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also see a problem with the apply and restore functionality. It could be how I implemented it in Nock. I'll continue to investigate this tomorrow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mikicho, thanks for the tests! I will look at them once I have a moment.
Also, I updated the minimum node version in the package.json to 18.20.0
Please, this would be best introduced on its own. Let's not add it as a part of this pull request.
The "cannot set symbol" error is caused because something tries to set it when it's already set. Interesting, I thought I put guards against that.
The errors disappear when I set writable: true for kRawHeaders.
This isn't a solution, it simply means one actor can override previously stored raw headers. This is actually quite dangerous because fetch api primitives repeatedly construct Headers instances.
const h = new Headers()
// ^ This is one headers instance.
const r = new Response(null, { headers: h })
r.headers
// ^ And this will be a completely different
// headers instance constructed from "h" as init.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please, this would be best introduced on its own. Let's not add it as a part of this pull request.
Reverted the change
This isn't a solution...
Got you!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will look at that test today, see what I can find.
expect(res.rawHeaders).toEqual( | ||
expect.arrayContaining(['X-CustoM-HeadeR', 'Yes']) | ||
) | ||
expect(res.headers['x-custom-header']).toEqual('Yes') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great!
All the tests are passing. @mikicho, let me know about that apply/restore issue you found. Otherwise, I think this is good to go. |
Released: v0.32.2 🎉This has been released in v0.32.2! Make sure to always update to the latest version ( Predictable release automation by @ossjs/release. |
Changes
We are now patching the global
Headers
class to record its raw headers as they come in from the developer. That's not ideal but our proxy is transparent and doesn't change the behavior in any way, just gathers the input (header fields).The Fetch API disregards header fields case sensitivity on all layers:
Headers
, raw headers are lost.Request
/Response
and giving it aHeaders
instance, the original header fields are lost.