From d3a74da19644477ffc81acf2a3efb26ad3335a5e Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Thu, 3 Oct 2024 09:21:11 +0100 Subject: [PATCH] fix: handle immutable response object (#12106) * fix: handle immutable response object * changeset --- .changeset/poor-doors-listen.md | 5 +++++ packages/astro/src/runtime/server/endpoint.ts | 20 ++++++++++++++----- .../fixtures/ssr-api-route/src/pages/fail.js | 3 +++ packages/astro/test/ssr-api-route.test.js | 20 ++++++++++++------- 4 files changed, 36 insertions(+), 12 deletions(-) create mode 100644 .changeset/poor-doors-listen.md create mode 100644 packages/astro/test/fixtures/ssr-api-route/src/pages/fail.js diff --git a/.changeset/poor-doors-listen.md b/.changeset/poor-doors-listen.md new file mode 100644 index 000000000000..098cf769104b --- /dev/null +++ b/.changeset/poor-doors-listen.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Handles case where an immutable Response object is returned from an endpoint diff --git a/packages/astro/src/runtime/server/endpoint.ts b/packages/astro/src/runtime/server/endpoint.ts index 900d604fd569..473cae180d6f 100644 --- a/packages/astro/src/runtime/server/endpoint.ts +++ b/packages/astro/src/runtime/server/endpoint.ts @@ -50,7 +50,7 @@ export async function renderEndpoint( return new Response(null, { status: 500 }); } - const response = await handler.call(mod, context); + let response = await handler.call(mod, context); if (!response || response instanceof Response === false) { throw new AstroError(EndpointDidNotReturnAResponse); @@ -59,10 +59,20 @@ export async function renderEndpoint( // Endpoints explicitly returning 404 or 500 response status should // NOT be subject to rerouting to 404.astro or 500.astro. if (REROUTABLE_STATUS_CODES.includes(response.status)) { - // Only `Response.redirect` headers are immutable, therefore a `try..catch` is not necessary. - // Note: `Response.redirect` can only be called with HTTP status codes: 301, 302, 303, 307, 308. - // Source: https://developer.mozilla.org/en-US/docs/Web/API/Response/redirect_static#parameters - response.headers.set(REROUTE_DIRECTIVE_HEADER, 'no'); + try { + response.headers.set(REROUTE_DIRECTIVE_HEADER, 'no'); + } catch (err) { + // In some cases the response may have immutable headers + // This is the case if, for example, the user directly returns a `fetch` response + // There's no clean way to check if the headers are immutable, so we just catch the error + // Note that response.clone() still has immutable headers! + if((err as Error).message?.includes('immutable')) { + response = new Response(response.body, response); + response.headers.set(REROUTE_DIRECTIVE_HEADER, 'no'); + } else { + throw err; + } + } } return response; diff --git a/packages/astro/test/fixtures/ssr-api-route/src/pages/fail.js b/packages/astro/test/fixtures/ssr-api-route/src/pages/fail.js new file mode 100644 index 000000000000..f9852dd93794 --- /dev/null +++ b/packages/astro/test/fixtures/ssr-api-route/src/pages/fail.js @@ -0,0 +1,3 @@ +export async function GET({ request }) { + return fetch("https://http.im/status/500", request) +} diff --git a/packages/astro/test/ssr-api-route.test.js b/packages/astro/test/ssr-api-route.test.js index ecdf458472e7..8e9c1bb5efa8 100644 --- a/packages/astro/test/ssr-api-route.test.js +++ b/packages/astro/test/ssr-api-route.test.js @@ -137,23 +137,29 @@ describe('API routes in SSR', () => { assert.equal(count, 2, 'Found two separate set-cookie response headers'); }); + it('can return an immutable response object', async () => { + const response = await fixture.fetch('/fail'); + const text = await response.text(); + assert.equal(response.status, 500); + assert.equal(text, ''); + }); + it('Has valid api context', async () => { const response = await fixture.fetch('/context/any'); assert.equal(response.status, 200); const data = await response.json(); - assert.equal(data.cookiesExist, true); - assert.equal(data.requestExist, true); - assert.equal(data.redirectExist, true); - assert.equal(data.propsExist, true); + assert.ok(data.cookiesExist); + assert.ok(data.requestExist); + assert.ok(data.redirectExist); + assert.ok(data.propsExist); assert.deepEqual(data.params, { param: 'any' }); assert.match(data.generator, /^Astro v/); - assert.equal( + assert.ok( ['http://[::1]:4321/blog/context/any', 'http://127.0.0.1:4321/blog/context/any'].includes( data.url, ), - true, ); - assert.equal(['::1', '127.0.0.1'].includes(data.clientAddress), true); + assert.ok(['::1', '127.0.0.1'].includes(data.clientAddress)); assert.equal(data.site, 'https://mysite.dev/subsite/'); }); });