diff --git a/packages/start-server-core/src/request-response.ts b/packages/start-server-core/src/request-response.ts index f8de03aa566..ab2b129b9d5 100644 --- a/packages/start-server-core/src/request-response.ts +++ b/packages/start-server-core/src/request-response.ts @@ -103,12 +103,37 @@ function attachResponseHeaders( event: H3Event, ): MaybePromise { if (isPromiseLike(value)) { - return value.then((resolved) => { - if (resolved instanceof Response) { - mergeEventResponseHeaders(resolved, event) - } - return resolved - }) + return value.then( + (resolved) => { + if (resolved instanceof Response) { + mergeEventResponseHeaders(resolved, event) + } + return resolved + }, + (error) => { + const eventStatus = event.res.status + if (eventStatus) { + const eventStatusText = event.res.statusText + const response = + error instanceof Response + ? new Response(error.body, { + status: eventStatus, + statusText: eventStatusText || error.statusText, + headers: error.headers, + }) + : new Response( + error instanceof Error ? error.message : String(error), + { + status: eventStatus, + statusText: eventStatusText || '', + }, + ) + mergeEventResponseHeaders(response, event) + return response as T + } + throw error + }, + ) } if (value instanceof Response) { diff --git a/packages/start-server-core/tests/request-response.test.ts b/packages/start-server-core/tests/request-response.test.ts new file mode 100644 index 00000000000..4a81ba0f2e6 --- /dev/null +++ b/packages/start-server-core/tests/request-response.test.ts @@ -0,0 +1,89 @@ +import { describe, expect, it } from 'vitest' + +import { + getRequest, + requestHandler, + setResponseStatus, +} from '../src/request-response' + +describe('setResponseStatus + throw preserves status code', () => { + it('should preserve status code when handler throws an Error after setResponseStatus', async () => { + const handler = requestHandler(async (_request: Request) => { + setResponseStatus(401) + throw new Error('Unauthorized') + }) + + const request = new Request('http://localhost/test') + const response = await handler(request, {}) + + expect(response.status).toBe(401) + expect(await response.text()).toBe('Unauthorized') + }) + + it('should preserve status code when handler throws a bare Response after setResponseStatus', async () => { + const handler = requestHandler(async (_request: Request) => { + setResponseStatus(429, 'Too Many Requests') + throw new Response('Rate limited') + }) + + const request = new Request('http://localhost/test') + const response = await handler(request, {}) + + expect(response.status).toBe(429) + }) + + it('should preserve status code when thrown Response already has same status', async () => { + const handler = requestHandler(async (_request: Request) => { + setResponseStatus(403) + throw new Response('Forbidden', { status: 403 }) + }) + + const request = new Request('http://localhost/test') + const response = await handler(request, {}) + + expect(response.status).toBe(403) + }) + + it('should transfer event status when handler returns non-Response value', async () => { + const handler = requestHandler(async (_request: Request) => { + setResponseStatus(204) + return null as any + }) + + const request = new Request('http://localhost/test') + const response = await handler(request, {}) + + expect(response.status).toBe(204) + }) + + it('should return 500 when handler throws without setResponseStatus', async () => { + const handler = requestHandler(async (_request: Request) => { + throw new Error('unexpected failure') + }) + + const request = new Request('http://localhost/test') + const response = await handler(request, {}) + + expect(response.status).toBe(500) + }) +}) + +describe('requestHandler basic behavior', () => { + it('should handle async handler returning a Response', async () => { + const handler = requestHandler(async (_request: Request) => { + return new Response('Hello', { status: 200 }) + }) + + const request = new Request('http://localhost/test') + const response = await handler(request, {}) + + expect(response.status).toBe(200) + expect(await response.text()).toBe('Hello') + }) + + it('should throw when accessing event outside requestHandler context', async () => { + expect(() => getRequest()).toThrow( + 'No StartEvent found in AsyncLocalStorage', + ) + }) +})