-
-
Notifications
You must be signed in to change notification settings - Fork 680
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
feat(context): make fetch Response headers mutable #3318
Conversation
Hi @nitedani, Thanks for making the pull request! Referencing internal symbols is a very interesting implementation, but in the main body of the hono we want to ensure that it is implemented only in runtime-independent code, within the scope of the web standard. Also, as we want to maintain high performance with simple requests, we usually do not want to include processing for ‘immutable response objects’. About #3316, I would prefer the following approach. diff --git a/src/context.ts b/src/context.ts
index e3dbc3c5..487e1456 100644
--- a/src/context.ts
+++ b/src/context.ts
@@ -465,16 +465,32 @@ export class Context<
set res(_res: Response | undefined) {
this.#isFresh = false
if (this.#res && _res) {
- this.#res.headers.delete('content-type')
- for (const [k, v] of this.#res.headers.entries()) {
- if (k === 'set-cookie') {
- const cookies = this.#res.headers.getSetCookie()
- _res.headers.delete('set-cookie')
- for (const cookie of cookies) {
- _res.headers.append('set-cookie', cookie)
+ try {
+ for (const [k, v] of this.#res.headers.entries()) {
+ if (k === 'content-type') {
+ continue
}
+
+ if (k === 'set-cookie') {
+ const cookies = this.#res.headers.getSetCookie()
+ _res.headers.delete('set-cookie')
+ for (const cookie of cookies) {
+ _res.headers.append('set-cookie', cookie)
+ }
+ } else {
+ _res.headers.set(k, v)
+ }
+ }
+ } catch (e) {
+ if (e instanceof TypeError && e.message.includes('immutable')) {
+ // `_res` is immutable (probably a response from a fetch API), so retry with a new response.
+ this.res = new Response(_res.body, {
+ headers: _res.headers,
+ status: _res.status,
+ })
+ return
} else {
- _res.headers.set(k, v)
+ throw e
}
}
} |
src/context.ts
Outdated
for (const cookie of cookies) { | ||
_res.headers.append('set-cookie', cookie) | ||
} | ||
} else if (!_res.headers.has(k)) { |
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.
Why wasn't this checked before? It feels unintended to overwrite the fresh headers with the old ones. Anyways, this would be a breaking change so I'll keep it out of the PR.
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 think that point is correct. I think the new response headers should be prioritised. And you're right, it would be a spec change and should be discussed in another PR.
That fails the added test Edge case: calling const res = fetch(...)
await res.text()
ctx.res = res // TypeError: Response body object should not be disturbed or locked |
Thanks for the answer. I think the test needs to be modified. I don't think ‘ c.res = res
c.res.headers.set('X-Custom', 'Message')
expect(c.res.headers.get('X-Custom')).toBe('Message') What is needed is a test of the following. However, it is debatable whether it is appropriate to use the external service https://jsonplaceholder.typicode.com in unit tests here. it('Should be able to overwrite a fetch reponse with a new response.', async () => {
c.res = await fetch('https://jsonplaceholder.typicode.com/todos/1')
c.res = new Response('foo', {
headers: {
'X-Custom': 'Message',
},
})
expect(c.res.headers.get('X-Custom')).toBe('Message')
})
it('Should be able to overwrite a response with a fetch response.', async () => {
c.res = new Response('foo', {
headers: {
'X-Custom': 'Message',
},
})
c.res = await fetch('https://jsonplaceholder.typicode.com/todos/1')
expect(c.res.headers.get('X-Custom')).toBe('Message')
}) |
The following cases are problems with the code itself. It is not possible to use the const res = fetch(...)
await res.text()
ctx.res = res |
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## next #3318 +/- ##
==========================================
- Coverage 96.31% 95.77% -0.54%
==========================================
Files 151 152 +1
Lines 15368 9201 -6167
Branches 2693 2822 +129
==========================================
- Hits 14801 8812 -5989
+ Misses 567 389 -178 ☔ View full report in Codecov by Sentry. |
I agree. Should we close the PR? Maybe we can support middleware like compression for fetch responses some other way? |
Hi @nitedani, Thanks for your comment. The following branch is the one with tests added to the contents of #3318 (comment). main...usualoma:hono:fix/update-context-res As for #3316, I believe this is how it should be fixed. It is up to you whether you want to reflect this in this pull request or close this pull request once and make another pull request. |
Sorry for the delayed response. This will be awesome PR! Sometimes, I have trouble with a matter like #3316 when I The code @usualoma wrote #3318 (comment) and #3318 (comment) looks good. Would you like us to go with it? |
Hi @nitedani Can you work on this? If you can't, I'll do it. |
@yusukebe Please take it. |
Hi @usualoma I've updated the @nitedani 's branch with applying your patch and pushing it. The test passed on my machine, but the CI is falling. Do you have any idea? |
@yusukebe With the changes in #3317, small data is no longer compressed, so the test needs to be large as well. diff --git a/runtime_tests/node/index.test.ts b/runtime_tests/node/index.test.ts
index b55512d5..310ec9c7 100644
--- a/runtime_tests/node/index.test.ts
+++ b/runtime_tests/node/index.test.ts
@@ -207,10 +207,11 @@ describe('streamSSE', () => {
})
describe('compress', async () => {
+ const cssContent = Array.from({ length: 60 }, () => 'body { color: red; }').join('\n')
const [externalServer, externalPort] = await new Promise<[Server, number]>((resolve) => {
const externalApp = new Hono()
externalApp.get('/style.css', (c) =>
- c.text('body { color: red; }', {
+ c.text(cssContent, {
headers: {
'Content-Type': 'text/css',
},
@@ -242,6 +243,6 @@ describe('compress', async () => {
const res = await request(server).get('/fetch/style.css')
expect(res.status).toBe(200)
expect(res.headers['content-encoding']).toBe('gzip')
- expect(res.text).toBe('body { color: red; }')
+ expect(res.text).toBe(cssContent)
})
})
|
f3d733b
to
00d4bf1
Compare
Cooool! It passed. Can you review this again, though it should be okay? |
@yusukebe Thank you, LGTM! |
The added test fails without the PR.
Fixes:
Return a piped response with compression enabled causes an unexpected error #3316
Add tests
Run tests
bun run format:fix && bun run lint:fix
to format the code