From 344e9bc480a075161a7811b7733593556e7560da Mon Sep 17 00:00:00 2001 From: Vitalii Rybachenko Date: Thu, 13 Feb 2025 06:22:19 -0500 Subject: [PATCH] =?UTF-8?q?fix:=20default=20head=20requests=20for=20endpoi?= =?UTF-8?q?nts=20when=20no=20explicit=20head=20method=E2=80=A6=20(#13210)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: default head requests for endpoints when no explicit head method implemented * Update changeset for astro to minor * Update changeset for default HEAD requests * Update .changeset/afraid-turkeys-kneel.md Co-authored-by: Reuben Tier <64310361+TheOtterlord@users.noreply.github.com> --------- Co-authored-by: Matt Kane Co-authored-by: Reuben Tier <64310361+TheOtterlord@users.noreply.github.com> --- .changeset/afraid-turkeys-kneel.md | 7 ++++ packages/astro/src/runtime/server/endpoint.ts | 13 ++++++-- packages/astro/test/core-image.test.js | 10 ++++++ .../test/units/runtime/endpoints.test.js | 33 +++++++++++++++++++ 4 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 .changeset/afraid-turkeys-kneel.md diff --git a/.changeset/afraid-turkeys-kneel.md b/.changeset/afraid-turkeys-kneel.md new file mode 100644 index 0000000000..34956a4ff3 --- /dev/null +++ b/.changeset/afraid-turkeys-kneel.md @@ -0,0 +1,7 @@ +--- +'astro': minor +--- + +Handle `HEAD` requests to an endpoint when a handler is not defined. + +If an endpoint defines a handler for `GET`, but does not define a handler for `HEAD`, Astro will call the `GET` handler and return the headers and status but an empty body. diff --git a/packages/astro/src/runtime/server/endpoint.ts b/packages/astro/src/runtime/server/endpoint.ts index 1a6bbc08e8..46ef8aa9d4 100644 --- a/packages/astro/src/runtime/server/endpoint.ts +++ b/packages/astro/src/runtime/server/endpoint.ts @@ -19,8 +19,12 @@ export async function renderEndpoint( const method = request.method.toUpperCase(); // use the exact match on `method`, fallback to ALL - const handler = mod[method] ?? mod['ALL']; - if (isPrerendered && method !== 'GET') { + let handler = mod[method] ?? mod['ALL']; + // use GET handler for HEAD requests + if (!handler && method === 'HEAD' && mod['GET']) { + handler = mod['GET']; + } + if (isPrerendered && !['GET', 'HEAD'].includes(method)) { logger.warn( 'router', `${url.pathname} ${bold( @@ -78,5 +82,10 @@ export async function renderEndpoint( } } + if (method === 'HEAD') { + // make sure HEAD responses doesnt have body + return new Response(null, response); + } + return response; } diff --git a/packages/astro/test/core-image.test.js b/packages/astro/test/core-image.test.js index a2ebfb88d4..914957c9c6 100644 --- a/packages/astro/test/core-image.test.js +++ b/packages/astro/test/core-image.test.js @@ -1135,6 +1135,16 @@ describe('astro:image', () => { assert.equal(response.headers.get('content-type'), 'image/webp'); }); + it('returns HEAD method ok for /_image', async () => { + const params = new URLSearchParams(); + params.set('href', '/src/assets/penguin1.jpg?origWidth=207&origHeight=243&origFormat=jpg'); + params.set('f', 'webp'); + const response = await fixture.fetch('/some-base/_image?' + String(params), { method: 'HEAD' }); + assert.equal(response.status, 200); + assert.equal(response.body, null); + assert.equal(response.headers.get('content-type'), 'image/webp'); + }); + it('does not interfere with query params', async () => { let res = await fixture.fetch('/api?src=image.png'); const html = await res.text(); diff --git a/packages/astro/test/units/runtime/endpoints.test.js b/packages/astro/test/units/runtime/endpoints.test.js index 313453c63d..2dfac0aa37 100644 --- a/packages/astro/test/units/runtime/endpoints.test.js +++ b/packages/astro/test/units/runtime/endpoints.test.js @@ -12,6 +12,7 @@ import { const root = new URL('../../fixtures/api-routes/', import.meta.url); const fileSystem = { '/src/pages/incorrect.ts': `export const GET = _ => {}`, + '/src/pages/headers.ts': `export const GET = () => { return new Response('content', { status: 201, headers: { Test: 'value' } }) }`, }; describe('endpoints', () => { @@ -44,4 +45,36 @@ describe('endpoints', () => { await done; assert.equal(res.statusCode, 500); }); + + it('should respond with 404 if GET is not implemented', async () => { + const { req, res, done } = createRequestAndResponse({ + method: 'HEAD', + url: '/incorrect-route', + }); + container.handle(req, res); + await done; + assert.equal(res.statusCode, 404); + }); + + it('should respond with same code as GET response', async () => { + const { req, res, done } = createRequestAndResponse({ + method: 'HEAD', + url: '/incorrect', + }); + container.handle(req, res); + await done; + assert.equal(res.statusCode, 500); // get not returns response + }); + + it('should remove body and pass headers for HEAD requests', async () => { + const { req, res, done } = createRequestAndResponse({ + method: 'HEAD', + url: '/headers', + }); + container.handle(req, res); + await done; + assert.equal(res.statusCode, 201); + assert.equal(res.getHeaders().test, 'value'); + assert.equal(res.body, undefined); + }); });