diff --git a/.changeset/rename-http-error.md b/.changeset/rename-http-error.md new file mode 100644 index 00000000000..e34ed518be0 --- /dev/null +++ b/.changeset/rename-http-error.md @@ -0,0 +1,5 @@ +--- +'@qwik.dev/router': minor +--- + +feat: `ServerError` is now `HttpError` and `requestEvent.error()` is now `requestEvent.httpError()`; the old names still work but are deprecated. diff --git a/e2e/qwik-e2e/apps/e2e-library/src/index.tsx b/e2e/qwik-e2e/apps/e2e-library/src/index.tsx index f2621d82460..d42a39ae86b 100644 --- a/e2e/qwik-e2e/apps/e2e-library/src/index.tsx +++ b/e2e/qwik-e2e/apps/e2e-library/src/index.tsx @@ -8,7 +8,7 @@ import { useTask$, } from '@qwik.dev/core'; import { getBasePathname, getRequestEvent, getRoutes, Link, server$ } from '@qwik.dev/router'; -import { ServerError } from '@qwik.dev/router/middleware/request-handler'; +import { HttpError } from '@qwik.dev/router/middleware/request-handler'; export interface Greeting { greeting: string; @@ -55,7 +55,7 @@ const readRequestPath = server$(function () { }); const rejectRequest = server$(() => { - throw new ServerError(403, 'rejected by the library'); + throw new HttpError(403, 'rejected by the library'); }); /** Reads the request and the route config through the library's own copy of the router. */ diff --git a/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/loaders/loader-error/uncaught-server/index.tsx b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/loaders/loader-error/uncaught-server/index.tsx index 67dffaeb2ad..c7bbba46acc 100644 --- a/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/loaders/loader-error/uncaught-server/index.tsx +++ b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/loaders/loader-error/uncaught-server/index.tsx @@ -1,9 +1,9 @@ import { component$ } from '@qwik.dev/core'; import { routeLoader$, server$ } from '@qwik.dev/router'; -import { ServerError } from '@qwik.dev/router/middleware/request-handler'; +import { HttpError } from '@qwik.dev/router/middleware/request-handler'; export const serverError = server$(() => { - throw new ServerError(401, 'server-error-data'); + throw new HttpError(401, 'server-error-data'); }); const useCatchServerErrorInLoader = routeLoader$(async () => { diff --git a/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/server-func/server-error/index.tsx b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/server-func/server-error/index.tsx index 1e47b88226a..3cbc2475d6e 100644 --- a/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/server-func/server-error/index.tsx +++ b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/server-func/server-error/index.tsx @@ -1,5 +1,5 @@ import { server$ } from '@qwik.dev/router'; -import { ServerError } from '@qwik.dev/router/middleware/request-handler'; +import { HttpError } from '@qwik.dev/router/middleware/request-handler'; import { component$, useSignal, useVisibleTask$ } from '@qwik.dev/core'; import { delay } from '../../actions/login'; @@ -9,7 +9,7 @@ type ErrorReason = { }; const serverFunctionA = server$(async function a(): Promise { - throw new ServerError(401, { + throw new HttpError(401, { reason: 'my error', middleware: 'server-error-uncaught', }); diff --git a/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/server-func/server-error/loader/index.tsx b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/server-func/server-error/loader/index.tsx index d52519cc87a..071ea19f270 100644 --- a/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/server-func/server-error/loader/index.tsx +++ b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/server-func/server-error/loader/index.tsx @@ -1,16 +1,16 @@ import { component$ } from '@qwik.dev/core'; import { routeLoader$, server$ } from '@qwik.dev/router'; -import { ServerError } from '@qwik.dev/router/middleware/request-handler'; +import { HttpError } from '@qwik.dev/router/middleware/request-handler'; export const serverError = server$(() => { - throw new ServerError(401, 'loader-error-data'); + throw new HttpError(401, 'loader-error-data'); }); const useCatchServerErrorInLoader = routeLoader$(async () => { try { await serverError(); } catch (err: any) { - if (err instanceof ServerError && typeof err.data === 'string') { + if (err instanceof HttpError && typeof err.data === 'string') { return err.data; } } diff --git a/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/server-func/server-error/primitive/index.tsx b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/server-func/server-error/primitive/index.tsx index 0c3fdeecd14..820f7eca74f 100644 --- a/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/server-func/server-error/primitive/index.tsx +++ b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/server-func/server-error/primitive/index.tsx @@ -1,13 +1,13 @@ import { component$, useSignal, useVisibleTask$ } from '@qwik.dev/core'; import { server$ } from '@qwik.dev/router'; -import { ServerError } from '@qwik.dev/router/middleware/request-handler'; +import { HttpError } from '@qwik.dev/router/middleware/request-handler'; const serverFunctionA = server$(async () => { - throw new ServerError(401, 1); + throw new HttpError(401, 1); }); const serverFunctionB = server$(async () => { - throw new ServerError(500, 'error'); + throw new HttpError(500, 'error'); }); export default component$(() => { diff --git a/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/plugin@errors.tsx b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/plugin@errors.tsx index b717e41712b..38adbeccaad 100644 --- a/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/plugin@errors.tsx +++ b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/plugin@errors.tsx @@ -1,13 +1,13 @@ import { type RequestHandler } from '@qwik.dev/router'; -import { ServerError } from '@qwik.dev/router/middleware/request-handler'; +import { HttpError } from '@qwik.dev/router/middleware/request-handler'; import { isErrorReason } from './(common)/server-func/server-error'; export const onRequest: RequestHandler = async ({ next }) => { try { return await next(); } catch (err) { - // Intercept and update ServerErrors to test middleware - if (err instanceof ServerError) { + // Intercept and update HttpErrors to test middleware + if (err instanceof HttpError) { // Update for (common)/server-func/server-error if (isErrorReason(err.data)) { err.data.middleware = 'server-error-caught'; diff --git a/e2e/qwik-e2e/tests/external-library.e2e.ts b/e2e/qwik-e2e/tests/external-library.e2e.ts index b60d41de7ab..d0348dfc5dd 100644 --- a/e2e/qwik-e2e/tests/external-library.e2e.ts +++ b/e2e/qwik-e2e/tests/external-library.e2e.ts @@ -42,7 +42,7 @@ test.describe('externalized qwik library', () => { await expect(page.locator('#lib-routes')).toHaveText('true'); }); - test('a ServerError thrown by the external library keeps its status', async ({ page }) => { + test('a HttpError thrown by the external library keeps its status', async ({ page }) => { // The browser logs the 403 response, so this test does not assert a clean console. await page.goto('/external-library/'); diff --git a/e2e/qwik-e2e/tests/qwikrouter/loaders.e2e.ts b/e2e/qwik-e2e/tests/qwikrouter/loaders.e2e.ts index b2ca65048a3..43b17c17239 100644 --- a/e2e/qwik-e2e/tests/qwikrouter/loaders.e2e.ts +++ b/e2e/qwik-e2e/tests/qwikrouter/loaders.e2e.ts @@ -443,7 +443,7 @@ test.describe('loaders', () => { await expect(page.locator('#prop-unwrapped')).toHaveText('test'); }); - test('should modify ServerError in middleware', async ({ page }) => { + test('should modify HttpError in middleware', async ({ page }) => { const response = await page.goto('/qwikrouter-test/loaders/loader-error'); const contentType = await response?.headerValue('Content-Type'); const status = response?.status(); @@ -454,7 +454,7 @@ test.describe('loaders', () => { await expect(body).toContainText('loader-error-caught'); }); - test('should return html with uncaught ServerErrors thrown in loaders', async ({ page }) => { + test('should return html with uncaught HttpErrors thrown in loaders', async ({ page }) => { const response = await page.goto('/qwikrouter-test/loaders/loader-error/uncaught-server'); const contentType = await response?.headerValue('Content-Type'); const status = response?.status(); diff --git a/e2e/qwik-e2e/tests/qwikrouter/server.e2e.ts b/e2e/qwik-e2e/tests/qwikrouter/server.e2e.ts index 3023fd48bcc..3559d58c3f0 100644 --- a/e2e/qwik-e2e/tests/qwikrouter/server.e2e.ts +++ b/e2e/qwik-e2e/tests/qwikrouter/server.e2e.ts @@ -118,18 +118,18 @@ test.describe('server$', () => { await expect(serverConfigContainer).toContainText('POST--MyCustomValue-GET--MyCustomValue'); }); - test('should modify ServerError in middleware', async ({ page }) => { + test('should modify HttpError in middleware', async ({ page }) => { await page.goto('/qwikrouter-test/server-func/server-error'); const serverConfigContainer = page.locator('#server-error'); await expect(serverConfigContainer).toContainText('my errorserver-error-caughtPOST'); }); - test('should catch ServerError in routeLoader', async ({ page }) => { + test('should catch HttpError in routeLoader', async ({ page }) => { await page.goto('/qwikrouter-test/server-func/server-error/loader'); const serverConfigContainer = page.locator('#server-error'); await expect(serverConfigContainer).toContainText('loader-error-data'); }); - test('should allow primitive ServerError data', async ({ page }) => { + test('should allow primitive HttpError data', async ({ page }) => { await page.goto('/qwikrouter-test/server-func/server-error/primitive'); const serverConfigContainer = page.locator('#server-error'); await expect(serverConfigContainer).toContainText('1error'); diff --git a/packages/docs/src/routes/api/qwik-router-middleware-request-handler/api.json b/packages/docs/src/routes/api/qwik-router-middleware-request-handler/api.json index e29a749812f..4e7813d6c7d 100644 --- a/packages/docs/src/routes/api/qwik-router-middleware-request-handler/api.json +++ b/packages/docs/src/routes/api/qwik-router-middleware-request-handler/api.json @@ -150,20 +150,20 @@ }, { "name": "data", - "id": "servererror-data", + "id": "httperror-data", "hierarchy": [ { - "name": "ServerError", - "id": "servererror-data" + "name": "HttpError", + "id": "httperror-data" }, { "name": "data", - "id": "servererror-data" + "id": "httperror-data" } ], "kind": "Property", "content": "```typescript\ndata: T;\n```", - "mdFile": "router.servererror.data.md" + "mdFile": "router.httperror.data.md" }, { "name": "DeferReturn", @@ -292,6 +292,20 @@ "content": "Returns an array of all the set `Response` `Set-Cookie` header values.\n\n\n```typescript\nheaders(): string[];\n```\n**Returns:**\n\nstring\\[\\]", "mdFile": "router.cookie.headers.md" }, + { + "name": "HttpError", + "id": "httperror", + "hierarchy": [ + { + "name": "HttpError", + "id": "httperror" + } + ], + "kind": "Class", + "content": "```typescript\nexport declare class HttpError extends Error \n```\n**Extends:** Error\n\n\n\n\n
\n\nConstructor\n\n\n\n\nModifiers\n\n\n\n\nDescription\n\n\n
\n\n(constructor)(status, data)\n\n\n\n\n\n\n\nConstructs a new instance of the `HttpError` class\n\n\n
\n\n\n\n\n\n
\n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\n[data](#httperror-data)\n\n\n\n\n\n\n\nT\n\n\n\n\n\n
\n\n[status](#httperror-status)\n\n\n\n\n\n\n\nnumber\n\n\n\n\n\n
", + "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/middleware/request-handler/http-error.ts", + "mdFile": "router.httperror.md" + }, { "name": "InternalRequest", "id": "internalrequest", @@ -403,7 +417,7 @@ } ], "kind": "Interface", - "content": "```typescript\nexport interface RequestEventCommon extends RequestEventBase \n```\n**Extends:** [RequestEventBase](#requesteventbase)<PLATFORM>\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nerror\n\n\n\n\n`readonly`\n\n\n\n\n<T = any>(statusCode: ErrorCodes, message: T) => [ServerError](#servererror)<T>\n\n\n\n\nWhen called, the response will immediately end with the given status code. This could be useful to end a response with `404`, and use the 404 handler in the routes directory. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status for which status code should be used.\n\n\n
\n\nexit\n\n\n\n\n`readonly`\n\n\n\n\n() => [AbortMessage](#abortmessage)\n\n\n\n\n\n
\n\nhtml\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: StatusCodes, html: string) => [AbortMessage](#abortmessage)\n\n\n\n\nConvenience method to send an HTML body response. The response will be automatically set the `Content-Type` header to`text/html; charset=utf-8`. An `html()` response can only be called once.\n\n\n
\n\njson\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: StatusCodes, data: any) => [AbortMessage](#abortmessage)\n\n\n\n\nConvenience method to JSON stringify the data and send it in the response. The response will be automatically set the `Content-Type` header to `application/json; charset=utf-8`. A `json()` response can only be called once.\n\n\n
\n\nlocale\n\n\n\n\n`readonly`\n\n\n\n\n(local?: string) => string\n\n\n\n\nWhich locale the content is in.\n\nThe locale value can be retrieved from selected methods using `getLocale()`:\n\n\n
\n\nredirect\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: RedirectCode, url: string) => [RedirectMessage](#redirectmessage)\n\n\n\n\nURL to redirect to. When called, the response will immediately end with the correct redirect status and headers.\n\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections\n\n\n
\n\nrewrite\n\n\n\n\n`readonly`\n\n\n\n\n(pathname: string) => [RewriteMessage](#rewritemessage)\n\n\n\n\nWhen called, qwik-router will execute the path's matching route flow.\n\nThe url in the browser will remain unchanged.\n\n\n
\n\nsend\n\n\n\n\n`readonly`\n\n\n\n\nSendMethod\n\n\n\n\nSend a body response. The `Content-Type` response header is not automatically set when using `send()` and must be set manually. A `send()` response can only be called once.\n\n\n
\n\nstatus\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode?: StatusCodes) => number\n\n\n\n\nHTTP response status code. Sets the status code when called with an argument. Always returns the status code, so calling `status()` without an argument will can be used to return the current status code.\n\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Status\n\n\n
\n\ntext\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: StatusCodes, text: string) => [AbortMessage](#abortmessage)\n\n\n\n\nConvenience method to send an text body response. The response will be automatically set the `Content-Type` header to`text/plain; charset=utf-8`. An `text()` response can only be called once.\n\n\n
", + "content": "```typescript\nexport interface RequestEventCommon extends RequestEventBase \n```\n**Extends:** [RequestEventBase](#requesteventbase)<PLATFORM>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nerror\n\n\n\n\n`readonly`\n\n\n\n\n<T = any>(statusCode: ErrorCodes, message: T) => [HttpError](#httperror)<T>\n\n\n\n\nThe old name of `httpError()`.\n\n\n
\n\nexit\n\n\n\n\n`readonly`\n\n\n\n\n() => [AbortMessage](#abortmessage)\n\n\n\n\n\n
\n\nhtml\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: StatusCodes, html: string) => [AbortMessage](#abortmessage)\n\n\n\n\nConvenience method to send an HTML body response. The response will be automatically set the `Content-Type` header to`text/html; charset=utf-8`. An `html()` response can only be called once.\n\n\n
\n\nhttpError\n\n\n\n\n`readonly`\n\n\n\n\n<T = any>(statusCode: ErrorCodes, message: T) => [HttpError](#httperror)<T>\n\n\n\n\nReturns an `HttpError` to throw, like `new HttpError(statusCode, message)`. Thrown from middleware or a blocking loader, it ends the response with that status and renders the nearest `error.tsx`. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status for which status code should be used.\n\n\n
\n\njson\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: StatusCodes, data: any) => [AbortMessage](#abortmessage)\n\n\n\n\nConvenience method to JSON stringify the data and send it in the response. The response will be automatically set the `Content-Type` header to `application/json; charset=utf-8`. A `json()` response can only be called once.\n\n\n
\n\nlocale\n\n\n\n\n`readonly`\n\n\n\n\n(local?: string) => string\n\n\n\n\nWhich locale the content is in.\n\nThe locale value can be retrieved from selected methods using `getLocale()`:\n\n\n
\n\nredirect\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: RedirectCode, url: string) => [RedirectMessage](#redirectmessage)\n\n\n\n\nURL to redirect to. When called, the response will immediately end with the correct redirect status and headers.\n\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections\n\n\n
\n\nrewrite\n\n\n\n\n`readonly`\n\n\n\n\n(pathname: string) => [RewriteMessage](#rewritemessage)\n\n\n\n\nWhen called, qwik-router will execute the path's matching route flow.\n\nThe url in the browser will remain unchanged.\n\n\n
\n\nsend\n\n\n\n\n`readonly`\n\n\n\n\nSendMethod\n\n\n\n\nSend a body response. The `Content-Type` response header is not automatically set when using `send()` and must be set manually. A `send()` response can only be called once.\n\n\n
\n\nstatus\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode?: StatusCodes) => number\n\n\n\n\nHTTP response status code. Sets the status code when called with an argument. Always returns the status code, so calling `status()` without an argument will can be used to return the current status code.\n\nhttps://developer.mozilla.org/en-US/docs/Web/HTTP/Status\n\n\n
\n\ntext\n\n\n\n\n`readonly`\n\n\n\n\n(statusCode: StatusCodes, text: string) => [AbortMessage](#abortmessage)\n\n\n\n\nConvenience method to send an text body response. The response will be automatically set the `Content-Type` header to`text/plain; charset=utf-8`. An `text()` response can only be called once.\n\n\n
", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/middleware/request-handler/types.ts", "mdFile": "router.requesteventcommon.md" }, @@ -431,7 +445,7 @@ } ], "kind": "Function", - "content": "```typescript\nexport type RequestHandler = (ev: RequestEvent) => Promise | void | AbortMessage | ServerError;\n```\n**References:** [RequestEvent](#requestevent), [AbortMessage](#abortmessage), [ServerError](#servererror)", + "content": "```typescript\nexport type RequestHandler = (ev: RequestEvent) => Promise | void | AbortMessage | HttpError;\n```\n**References:** [RequestEvent](#requestevent), [AbortMessage](#abortmessage), [HttpError](#httperror)", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/middleware/request-handler/request-handler.ts", "mdFile": "router.requesthandler.md" }, @@ -445,7 +459,7 @@ } ], "kind": "TypeAlias", - "content": "```typescript\nexport type RequestHandler = (ev: RequestEvent) => Promise | void | AbortMessage | ServerError;\n```\n**References:** [RequestEvent](#requestevent), [AbortMessage](#abortmessage), [ServerError](#servererror)", + "content": "```typescript\nexport type RequestHandler = (ev: RequestEvent) => Promise | void | AbortMessage | HttpError;\n```\n**References:** [RequestEvent](#requestevent), [AbortMessage](#abortmessage), [HttpError](#httperror)", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/middleware/request-handler/types.ts", "mdFile": "router.requesthandler.md" }, @@ -500,9 +514,23 @@ "id": "servererror" } ], - "kind": "Class", - "content": "```typescript\nexport declare class ServerError extends Error \n```\n**Extends:** Error\n\n\n\n\n
\n\nConstructor\n\n\n\n\nModifiers\n\n\n\n\nDescription\n\n\n
\n\n(constructor)(status, data)\n\n\n\n\n\n\n\nConstructs a new instance of the `ServerError` class\n\n\n
\n\n\n\n\n\n
\n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\n[data](#servererror-data)\n\n\n\n\n\n\n\nT\n\n\n\n\n\n
\n\n[status](#servererror-status)\n\n\n\n\n\n\n\nnumber\n\n\n\n\n\n
", - "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/middleware/request-handler/server-error.ts", + "kind": "TypeAlias", + "content": "> Warning: This API is now obsolete.\n> \n> Use `HttpError`.\n> \n\nThe old name of `HttpError`.\n\n\n```typescript\nServerError: typeof HttpError\n```", + "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/middleware/request-handler/http-error.ts", + "mdFile": "router.servererror.md" + }, + { + "name": "ServerError", + "id": "servererror", + "hierarchy": [ + { + "name": "ServerError", + "id": "servererror" + } + ], + "kind": "Variable", + "content": "> Warning: This API is now obsolete.\n> \n> Use `HttpError`.\n> \n\nThe old name of `HttpError`.\n\n\n```typescript\nServerError: typeof HttpError\n```", + "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/middleware/request-handler/http-error.ts", "mdFile": "router.servererror.md" }, { @@ -580,20 +608,20 @@ }, { "name": "status", - "id": "servererror-status", + "id": "httperror-status", "hierarchy": [ { - "name": "ServerError", - "id": "servererror-status" + "name": "HttpError", + "id": "httperror-status" }, { "name": "status", - "id": "servererror-status" + "id": "httperror-status" } ], "kind": "Property", "content": "```typescript\nstatus: number;\n```", - "mdFile": "router.servererror.status.md" + "mdFile": "router.httperror.status.md" } ] } \ No newline at end of file diff --git a/packages/docs/src/routes/api/qwik-router-middleware-request-handler/index.mdx b/packages/docs/src/routes/api/qwik-router-middleware-request-handler/index.mdx index 84214f9680e..9765e1dd21c 100644 --- a/packages/docs/src/routes/api/qwik-router-middleware-request-handler/index.mdx +++ b/packages/docs/src/routes/api/qwik-router-middleware-request-handler/index.mdx @@ -566,7 +566,7 @@ string [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/middleware/request-handler/types.ts) -

data

+

data

```typescript data: T; @@ -806,6 +806,87 @@ headers(): string[]; string[] +

HttpError

+ +```typescript +export declare class HttpError extends Error +``` + +**Extends:** Error + + + +
+ +Constructor + + + +Modifiers + + + +Description + +
+ +(constructor)(status, data) + + + + + +Constructs a new instance of the `HttpError` class + +
+ + + + +
+ +Property + + + +Modifiers + + + +Type + + + +Description + +
+ +[data](#httperror-data) + + + + + +T + + + +
+ +[status](#httperror-status) + + + + + +number + + + +
+ +[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/middleware/request-handler/http-error.ts) +

InternalRequest

Internal JSON request kind handled by Qwik Router, or `false` for normal page requests. @@ -1412,11 +1493,11 @@ error -<T = any>(statusCode: ErrorCodes, message: T) => [ServerError](#servererror)<T> +<T = any>(statusCode: ErrorCodes, message: T) => [HttpError](#httperror)<T> -When called, the response will immediately end with the given status code. This could be useful to end a response with `404`, and use the 404 handler in the routes directory. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status for which status code should be used. +The old name of `httpError()`. @@ -1453,6 +1534,23 @@ Convenience method to send an HTML body response. The response will be automatic +httpError + + + +`readonly` + + + +<T = any>(statusCode: ErrorCodes, message: T) => [HttpError](#httperror)<T> + + + +Returns an `HttpError` to throw, like `new HttpError(statusCode, message)`. Thrown from middleware or a blocking loader, it ends the response with that status and renders the nearest `error.tsx`. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status for which status code should be used. + + + + json @@ -1642,14 +1740,10 @@ resolveValue ```typescript export type RequestHandler = ( ev: RequestEvent, -) => - | Promise - | void - | AbortMessage - | ServerError; +) => Promise | void | AbortMessage | HttpError; ``` -**References:** [RequestEvent](#requestevent), [AbortMessage](#abortmessage), [ServerError](#servererror) +**References:** [RequestEvent](#requestevent), [AbortMessage](#abortmessage), [HttpError](#httperror) [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/middleware/request-handler/request-handler.ts) @@ -1658,14 +1752,10 @@ export type RequestHandler = ( ```typescript export type RequestHandler = ( ev: RequestEvent, -) => - | Promise - | void - | AbortMessage - | ServerError; +) => Promise | void | AbortMessage | HttpError; ``` -**References:** [RequestEvent](#requestevent), [AbortMessage](#abortmessage), [ServerError](#servererror) +**References:** [RequestEvent](#requestevent), [AbortMessage](#abortmessage), [HttpError](#httperror) [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/middleware/request-handler/types.ts) @@ -1755,86 +1845,33 @@ string [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/middleware/request-handler/rewrite-handler.ts) -

ServerError

- -```typescript -export declare class ServerError extends Error -``` - -**Extends:** Error - - - -
- -Constructor - - - -Modifiers - - - -Description - -
+

ServerError

-(constructor)(status, data) - -
- - +> Warning: This API is now obsolete. +> +> Use `HttpError`. -Constructs a new instance of the `ServerError` class - -
- - - - -
+The old name of `HttpError`. -Property - - - -Modifiers - - - -Type - - - -Description - -
- -[data](#servererror-data) - - - - - -T - - - -
- -[status](#servererror-status) +```typescript +ServerError: typeof HttpError; +``` - +[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/middleware/request-handler/http-error.ts) - +

ServerError

-number +> Warning: This API is now obsolete. +> +> Use `HttpError`. -
+The old name of `HttpError`. -
+```typescript +ServerError: typeof HttpError; +``` -[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/middleware/request-handler/server-error.ts) +[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/middleware/request-handler/http-error.ts)

ServerRenderOptions

@@ -2137,7 +2174,7 @@ _(Optional)_ void -

status

+

status

```typescript status: number; diff --git a/packages/docs/src/routes/api/qwik-router/api.json b/packages/docs/src/routes/api/qwik-router/api.json index c85e22b757f..74d38c8baef 100644 --- a/packages/docs/src/routes/api/qwik-router/api.json +++ b/packages/docs/src/routes/api/qwik-router/api.json @@ -306,7 +306,7 @@ } ], "kind": "TypeAlias", - "content": "Drops control-flow signals (`ev.redirect()`, `ev.error()`, etc.) from a loader/action return type: those are thrown, not surfaced as data. `ev.fail()` is plain data and is kept.\n\n\n```typescript\nexport type ExcludeControlFlow = Exclude;\n```", + "content": "Drops control-flow signals (`ev.redirect()`, `ev.error()`, etc.) from a loader/action return type: those are thrown, not surfaced as data. `ev.fail()` is plain data and is kept.\n\n\n```typescript\nexport type ExcludeControlFlow = Exclude;\n```", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/types.ts", "mdFile": "router.excludecontrolflow.md" }, diff --git a/packages/docs/src/routes/api/qwik-router/index.mdx b/packages/docs/src/routes/api/qwik-router/index.mdx index eb3c8da7651..13350e6e796 100644 --- a/packages/docs/src/routes/api/qwik-router/index.mdx +++ b/packages/docs/src/routes/api/qwik-router/index.mdx @@ -799,7 +799,7 @@ export type Editable = { Drops control-flow signals (`ev.redirect()`, `ev.error()`, etc.) from a loader/action return type: those are thrown, not surfaced as data. `ev.fail()` is plain data and is kept. ```typescript -export type ExcludeControlFlow = Exclude; +export type ExcludeControlFlow = Exclude; ``` [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/types.ts) diff --git a/packages/docs/src/routes/demo/qwikrouter/middleware/error/index.tsx b/packages/docs/src/routes/demo/qwikrouter/middleware/error/index.tsx index e20f8a1dd67..b4db3624e30 100644 --- a/packages/docs/src/routes/demo/qwikrouter/middleware/error/index.tsx +++ b/packages/docs/src/routes/demo/qwikrouter/middleware/error/index.tsx @@ -1,5 +1,5 @@ import { type RequestHandler } from '@qwik.dev/router'; -export const onGet: RequestHandler = async ({ error }) => { - throw error(500, 'ERROR: Demonstration of an error response.'); +export const onGet: RequestHandler = async ({ httpError }) => { + throw httpError(500, 'ERROR: Demonstration of an error response.'); }; diff --git a/packages/docs/src/routes/docs/(qwikrouter)/error-handling/index.mdx b/packages/docs/src/routes/docs/(qwikrouter)/error-handling/index.mdx index f69060cf8cb..0f3622585ec 100644 --- a/packages/docs/src/routes/docs/(qwikrouter)/error-handling/index.mdx +++ b/packages/docs/src/routes/docs/(qwikrouter)/error-handling/index.mdx @@ -12,30 +12,30 @@ import { Note } from '~/components/note/note'; When an error is thrown in a loader or `server$` function, a 500 error is returned to the client along with the error. This is useful during development but isn't always desirable for production systems. Qwik provides the tools necessary to customise how errors are handled. -Throwing a `ServerError` instance allows you to return custom errors to the browser with a different status code and serialised data. +Throwing a `HttpError` instance allows you to return custom errors to the browser with a different status code and serialised data. -Loaders also provide a helper function on the event object to easily create new ServerErrors. +Loaders also provide a helper function on the event object to easily create new HttpErrors. ```tsx -// Throw ServerErrors from a routerLoader$ +// Throw HttpErrors from a routerLoader$ const useProduct = routeLoader$(async (ev) => { const product = await fetch('api/product/1') if (!product) { // Throw a 404 with a custom payload - throw new ServerError(404, 'Product not found') + throw new HttpError(404, 'Product not found') // Or use the existing helper function - throw ev.error(404, 'Product not found') + throw ev.httpError(404, 'Product not found') } return product }) -// Throw ServerErrors from a server$ +// Throw HttpErrors from a server$ const getPrices = server$(() => { if (!isAuthenticated()) { - throw new ServerError(401, { code: 401 }) + throw new HttpError(401, { code: 401 }) } return fetch('api/product/1/prices') @@ -48,7 +48,7 @@ export default component$(() => { getPrices() .then() .catch(err => { - // The payload from a ServerError is deserialised as the error caught in the client + // The payload from a HttpError is deserialised as the error caught in the client if (err.code === 401) { // Navigate to login page } @@ -68,7 +68,7 @@ Intercepting errors with middleware has a few usecases: you might want to hide e ```tsx // src/routes/plugin@errors.ts import { type RequestHandler } from '@qwik.dev/router' -import { RedirectMessage, ServerError } from '@qwik.dev/router/middleware/request-handler' +import { RedirectMessage, HttpError } from '@qwik.dev/router/middleware/request-handler' import { isDev } from '@qwik.dev/core/build' export const onRequest: RequestHandler = async ({ next }) => { @@ -80,8 +80,8 @@ export const onRequest: RequestHandler = async ({ next }) => { throw err } - // Pass through ServerErrors - if (err instanceof ServerError) { + // Pass through HttpErrors + if (err instanceof HttpError) { throw err } @@ -91,7 +91,7 @@ export const onRequest: RequestHandler = async ({ next }) => { if (isDev) { throw err } else { - throw new ServerError(500, 'Internal server error'); + throw new HttpError(500, 'Internal server error'); } } }; diff --git a/packages/docs/src/routes/docs/(qwikrouter)/middleware/index.mdx b/packages/docs/src/routes/docs/(qwikrouter)/middleware/index.mdx index 3bc13bb3a20..8c903b4b76c 100644 --- a/packages/docs/src/routes/docs/(qwikrouter)/middleware/index.mdx +++ b/packages/docs/src/routes/docs/(qwikrouter)/middleware/index.mdx @@ -484,16 +484,16 @@ export const onGet: RequestHandler = async ({ redirect, url }) => { ``` -### `error()` +### `httpError()` -Set an error response. +Returns an `HttpError` to throw: the router renders the nearest `error.tsx` at its status. `throw new HttpError(status, message)` does the same. `error()` is its deprecated old name. ```tsx import { type RequestHandler } from '@qwik.dev/router'; -export const onGet: RequestHandler = async ({ error }) => { - throw error(500, 'ERROR: Demonstration of an error response.'); +export const onGet: RequestHandler = async ({ httpError }) => { + throw httpError(500, 'ERROR: Demonstration of an error response.'); }; ``` diff --git a/packages/docs/src/routes/docs/(qwikrouter)/route-loader/index.mdx b/packages/docs/src/routes/docs/(qwikrouter)/route-loader/index.mdx index 6438d613d61..09dc7c28733 100644 --- a/packages/docs/src/routes/docs/(qwikrouter)/route-loader/index.mdx +++ b/packages/docs/src/routes/docs/(qwikrouter)/route-loader/index.mdx @@ -55,7 +55,7 @@ A few properties of route loaders: - **Keyed by route, not by call site.** Two visits to the same URL share one logical "result" per "route + url params + search params" set, not one per `useProductDetails()` call. - **Result is a `ComputedSignal`.** Components read `.value`. While the data is loading, reading `.value` suspends; if the loader threw, it re-throws the error. - **Each loader has its own JSON endpoint.** The client fetches `q-loader-{id}.{hash}.json` for navigation, polling, and post-action invalidation. The browser HTTP cache, ETags, and the `cacheControl` option all apply. -- **Blocks SSR by default.** The server awaits the loader before rendering the page, so a `throw redirect()` or `throw error()` controls the response. Opt out with `blockSSR: false` to run it in the background — see [Background loaders](#background-loaders-blockssr). +- **Blocks SSR by default.** The server awaits the loader before rendering the page, so a `throw redirect()` or `throw httpError()` controls the response. Opt out with `blockSSR: false` to run it in the background — see [Background loaders](#background-loaders-blockssr). This means a `routeLoader$()` should be a **pure function of the URL**: same `params` and `search` → same response. Mixing in any other input (action state, request bodies, one-shot tokens) makes the cached responses inconsistent — see [Loaders cannot read action state](#loaders-cannot-read-action-state). @@ -99,7 +99,7 @@ export default component$(() => ( Loaders signal failure two ways: -- **`throw requestEvent.error(status, data)`** — throw a `ServerError` directly. Reading `signal.value` re-throws the `ServerError {status, data}`, so wrap the reader in a ``. This is the recommended way to signal failure from a loader. +- **`throw requestEvent.httpError(status, data)`** — throw an `HttpError` directly. Reading `signal.value` re-throws the `HttpError {status, data}`, so wrap the reader in a ``. This is the recommended way to signal failure from a loader. - **`return requestEvent.fail(status, data)`** — returns an inline failure. `signal.value.failed` is `true` and `signal.value` contains the rest of the data. In both cases the result is not cacheable. For a blocking loader (the default) the HTTP response status is also set to `status`; a `blockSSR: false` loader is isolated from the page response — see [Background loaders](#background-loaders-blockssr). @@ -274,7 +274,7 @@ When using lazy-loaded data, wrap the reader in `` to show a placeholde ## Background loaders (`blockSSR`) -By default a loader **blocks SSR**: the server awaits it before rendering, so a `throw redirect()` or `throw error()` short-circuits the HTTP response. When several loaders block, the first one to error, in route order, decides the error, however the last one to set the status, in chronological order, decides the status. +By default a loader **blocks SSR**: the server awaits it before rendering, so a `throw redirect()` or `throw httpError()` short-circuits the HTTP response. When several loaders block, the first one to error, in route order, decides the error, however the last one to set the status, in chronological order, decides the status. Set `blockSSR: false` to run a loader in the background instead. The server starts it but does not wait: rendering begins immediately, and reading `.value` suspends only that read until the loader resolves. This starts SSR sooner, and is the recommended setting, unless you need to control the request from the loader. @@ -287,7 +287,7 @@ export const useRecommendations = routeLoader$( ); ``` -A background loader is treated as a **separate request**: it cannot redirect or error the page response. If it calls `redirect()` or `error()`, that control flow is captured on its own signal (surfacing when `.value` is read) and never changes the page's status or headers. Keep route guards that must redirect or set a status in a [request handler](/docs/middleware) such as `onRequest`, or in a blocking loader. +A background loader is treated as a **separate request**: it cannot redirect or error the page response. If it calls `redirect()` or `httpError()`, that control flow is captured on its own signal (surfacing when `.value` is read) and never changes the page's status or headers. Keep route guards that must redirect or set a status in a [request handler](/docs/middleware) such as `onRequest`, or in a blocking loader. Note: when the client fetches a background loader for SPA navigation, there is no difference between a blocking and non-blocking loader — the SPA fetch is always async, and the page is already rendered. Errors are not surfaced directly, but redirects are followed automatically. diff --git a/packages/docs/src/routes/docs/upgrade/index.mdx b/packages/docs/src/routes/docs/upgrade/index.mdx index 9a388c29a3b..a58678a225a 100644 --- a/packages/docs/src/routes/docs/upgrade/index.mdx +++ b/packages/docs/src/routes/docs/upgrade/index.mdx @@ -431,6 +431,20 @@ const label: ReadonlySignal = useComputed$(() => name.value); const label: Readonly> = useComputed$(() => name.value); ``` +### `ServerError` → `HttpError`, `error()` → `httpError()` + +`ServerError` is now `HttpError`, and the request event's `error()` is now `httpError()`. The old names still work, but are deprecated: + +```typescript +// v1 +throw new ServerError(401, 'Sign in'); +throw requestEvent.error(404, 'Not found'); + +// v2 +throw new HttpError(401, 'Sign in'); +throw requestEvent.httpError(404, 'Not found'); +``` + ### qwik-labs `@builder.io/qwik-labs` is removed in v2: diff --git a/packages/insights/src/components/symbol-tile/index.tsx b/packages/insights/src/components/symbol-tile/index.tsx index f86103f6a42..ad8d801556b 100644 --- a/packages/insights/src/components/symbol-tile/index.tsx +++ b/packages/insights/src/components/symbol-tile/index.tsx @@ -1,5 +1,6 @@ import { Resource, component$, useResource$, useStore } from '@qwik.dev/core'; import { getRequestEvent, server$, useLocation, type RequestEvent } from '@qwik.dev/router'; +import { HttpError } from '@qwik.dev/router/middleware/request-handler'; import { and, eq } from 'drizzle-orm'; import { getDB, symbolDetailTable } from '~/db'; import { dbGetInsightUser, type InsightsUser } from '~/db/sql-user'; @@ -141,7 +142,7 @@ export function displaySymbolDetail(value: string | null | undefined): string { type LoadInsightsUser = (email: string) => Promise; export async function getAuthorizedPublicApiKey( - requestEvent: Pick, + requestEvent: Pick, requestedPublicApiKey = requestEvent.params.publicApiKey, loadUser: LoadInsightsUser = dbGetInsightUser ): Promise { @@ -153,7 +154,7 @@ export async function getAuthorizedPublicApiKey( (getInsightUser(requestEvent.sharedMap) as InsightsUser | undefined) ?? (email ? await loadUser(email) : undefined); if (!requestedPublicApiKey || !insightUser?.isAuthorizedForApp(requestedPublicApiKey)) { - throw requestEvent.error(403, 'Forbidden'); + throw new HttpError(403, 'Forbidden'); } return requestedPublicApiKey; } diff --git a/packages/insights/src/components/symbol-tile/index.unit.ts b/packages/insights/src/components/symbol-tile/index.unit.ts index 6fb3d66f6e7..656bd742575 100644 --- a/packages/insights/src/components/symbol-tile/index.unit.ts +++ b/packages/insights/src/components/symbol-tile/index.unit.ts @@ -1,5 +1,4 @@ import { describe, expect, test, vi } from 'vitest'; -import type { RequestEvent } from '@qwik.dev/router'; import { InsightsUser } from '~/db/sql-user'; vi.mock('@qwik.dev/router', () => ({ @@ -13,34 +12,25 @@ describe('symbol source tenant authorization', () => { test('rejects a request without an authenticated Insights user', async () => { (globalThis as any).__EXPERIMENTAL__ = {}; const { getAuthorizedPublicApiKey } = await import('./index'); - const forbidden = new Error('Forbidden'); - const error = vi.fn(() => forbidden); - await expect( getAuthorizedPublicApiKey({ params: { publicApiKey: 'app-a' }, sharedMap: new Map(), - error: error as unknown as RequestEvent['error'], }) - ).rejects.toBe(forbidden); + ).rejects.toMatchObject({ status: 403, message: 'Forbidden' }); }); test('rejects a tenant that the current user cannot access', async () => { (globalThis as any).__EXPERIMENTAL__ = {}; const { getAuthorizedPublicApiKey } = await import('./index'); - const forbidden = new Error('Forbidden'); - const error = vi.fn(() => forbidden); - await expect( getAuthorizedPublicApiKey({ params: { publicApiKey: 'app-b' }, sharedMap: new Map([ ['insightUser', new InsightsUser(1, 'john@example.com', false, ['app-a'])], ]), - error: error as unknown as RequestEvent['error'], }) - ).rejects.toBe(forbidden); - expect(error).toHaveBeenCalledWith(403, 'Forbidden'); + ).rejects.toMatchObject({ status: 403, message: 'Forbidden' }); }); test('uses the route tenant when the current user can access it', async () => { @@ -53,7 +43,6 @@ describe('symbol source tenant authorization', () => { sharedMap: new Map([ ['insightUser', new InsightsUser(1, 'john@example.com', false, ['app-a'])], ]), - error: vi.fn() as RequestEvent['error'], }) ).resolves.toBe('app-a'); }); @@ -69,7 +58,6 @@ describe('symbol source tenant authorization', () => { sharedMap: new Map([ ['insightUser', new InsightsUser(1, 'john@example.com', false, ['app-a'])], ]), - error: vi.fn() as RequestEvent['error'], }, 'app-a' ) @@ -88,7 +76,6 @@ describe('symbol source tenant authorization', () => { { params: {}, sharedMap: new Map([['session', { user: { email: 'john@example.com' } }]]), - error: vi.fn() as RequestEvent['error'], }, 'app-a', loadUser diff --git a/packages/qwik-router/ARCHITECTURE.md b/packages/qwik-router/ARCHITECTURE.md index b0af34ee216..3bca841fac0 100644 --- a/packages/qwik-router/ARCHITECTURE.md +++ b/packages/qwik-router/ARCHITECTURE.md @@ -24,7 +24,7 @@ calls `requestHandler(serverRequestEv, opts)`. | Handler | Description | | ----------------------- | --------------------------------------------------------- | | jsonRequestWrapper | wraps redirects/errors as JSON for loader/action fetches | -| serverErrorMiddleware | catches ServerError, swaps to error module, re-renders | +| serverErrorMiddleware | catches HttpError, swaps to error module, re-renders | | csrfCheck | POST/PUT/PATCH/DELETE origin check | | serverPlugins.onRequest | global plugin middleware | | routeModules.onRequest | per-route middleware (layouts + page) | diff --git a/packages/qwik-router/src/buildtime/runtime-generation/generate-routes.ts b/packages/qwik-router/src/buildtime/runtime-generation/generate-routes.ts index 561c25d17f7..da0016ca9f9 100644 --- a/packages/qwik-router/src/buildtime/runtime-generation/generate-routes.ts +++ b/packages/qwik-router/src/buildtime/runtime-generation/generate-routes.ts @@ -152,7 +152,7 @@ export function createRoutes( ); // Note: both error.tsx and 404.tsx in the same directory is fine. - // error.tsx (_E) handles ServerErrors (403, 500, etc.) and 404.tsx (_4) handles not-found routes. + // error.tsx (_E) handles HttpErrors (403, 500, etc.) and 404.tsx (_4) handles not-found routes. // They are tracked independently in the route trie. // Wrap the trie in the base pathname segments (e.g., '/qwikrouter-test/' → 'qwikrouter-test') diff --git a/packages/qwik-router/src/middleware/request-handler/handlers/action-handler.ts b/packages/qwik-router/src/middleware/request-handler/handlers/action-handler.ts index 2f44400a548..0fc63b85e7f 100644 --- a/packages/qwik-router/src/middleware/request-handler/handlers/action-handler.ts +++ b/packages/qwik-router/src/middleware/request-handler/handlers/action-handler.ts @@ -10,7 +10,7 @@ import type { import { RedirectMessage } from '../redirect-handler'; import { RequestEvSharedActionId, type RequestEventInternal } from '../request-event-core'; import { IsQAction, QActionId } from '../request-path'; -import { throwIfControlFlowSignal } from '../server-error'; +import { throwIfControlFlowSignal } from '../http-error'; import type { QRL } from '@qwik.dev/core'; import type { RequestEventBase } from '../types'; diff --git a/packages/qwik-router/src/middleware/request-handler/handlers/json-request-wrapper.ts b/packages/qwik-router/src/middleware/request-handler/handlers/json-request-wrapper.ts index 4e309e987cc..52a3df7a612 100644 --- a/packages/qwik-router/src/middleware/request-handler/handlers/json-request-wrapper.ts +++ b/packages/qwik-router/src/middleware/request-handler/handlers/json-request-wrapper.ts @@ -3,7 +3,7 @@ import { FULLPATH_HEADER } from '../../../runtime/src/route-loaders'; import { RedirectMessage } from '../redirect-handler'; import type { RequestEventInternal } from '../request-event-core'; import { resolveValidInternalFullPathname } from '../request-path'; -import { ServerError } from '../server-error'; +import { HttpError } from '../http-error'; import type { RequestHandler, RequestEvent } from '../types'; import { addVaryHeader, sendJsonResponse, sendActionResponse } from './loader-handler'; @@ -57,7 +57,7 @@ export function jsonRequestWrapper(): RequestHandler { // 3xx would be auto-followed by fetch, turning the action POST into a GET of the target. await sendActionResponse(requestEv, { redirect: location }); } - } else if (err instanceof ServerError) { + } else if (err instanceof HttpError) { requestEv.headers.set('Cache-Control', 'no-store'); if (isLoader) { await sendJsonResponse(requestEv, { e: err }); @@ -70,7 +70,7 @@ export function jsonRequestWrapper(): RequestHandler { const message = isDev ? `${err.message}\n(this is only visible in dev mode)` : 'Internal Server Error'; - const se = new ServerError(500, message); + const se = new HttpError(500, message); if (isLoader) { await sendJsonResponse(requestEv, { e: se }); } else { diff --git a/packages/qwik-router/src/middleware/request-handler/handlers/json-request-wrapper.unit.ts b/packages/qwik-router/src/middleware/request-handler/handlers/json-request-wrapper.unit.ts index bbd120118fa..fdad56798aa 100644 --- a/packages/qwik-router/src/middleware/request-handler/handlers/json-request-wrapper.unit.ts +++ b/packages/qwik-router/src/middleware/request-handler/handlers/json-request-wrapper.unit.ts @@ -3,7 +3,7 @@ import { _deserialize } from '@qwik.dev/core/internal'; import { FULLPATH_HEADER } from '../../../runtime/src/route-loaders'; import { createCacheControl } from '../cache-control'; import { RedirectMessage } from '../redirect-handler'; -import { ServerError } from '../server-error'; +import { HttpError } from '../http-error'; import { IsQLoader } from '../request-path'; import type { CacheControl } from '../types'; import { jsonRequestWrapper } from './json-request-wrapper'; @@ -52,9 +52,9 @@ describe('jsonRequestWrapper', () => { expect(result).toEqual({ r: '/login/' }); }); it.each([ - ['loader', 'a ServerError', new ServerError(403, 'Members only')], + ['loader', 'an HttpError', new HttpError(403, 'Members only')], ['loader', 'a plain Error', new Error('middleware boom')], - ['action', 'a ServerError', new ServerError(403, 'Members only')], + ['action', 'an HttpError', new HttpError(403, 'Members only')], ['action', 'a plain Error', new Error('middleware boom')], ] as const)( 'sends %s middleware failures from %s with Cache-Control: no-store', diff --git a/packages/qwik-router/src/middleware/request-handler/handlers/loader-handler.unit.ts b/packages/qwik-router/src/middleware/request-handler/handlers/loader-handler.unit.ts index e7ac65a187f..a8d2d6d9bed 100644 --- a/packages/qwik-router/src/middleware/request-handler/handlers/loader-handler.unit.ts +++ b/packages/qwik-router/src/middleware/request-handler/handlers/loader-handler.unit.ts @@ -3,7 +3,7 @@ import { FULLPATH_HEADER } from '../../../runtime/src/route-loaders'; import { createCacheControl } from '../cache-control'; import { getLoaderName, IsQLoader, QLoaderId } from '../request-path'; import { RedirectMessage } from '../redirect-handler'; -import { ServerError } from '../server-error'; +import { HttpError } from '../http-error'; import type { CacheControl, RequestEvent } from '../types'; import { loaderHandler } from './loader-handler'; @@ -287,13 +287,13 @@ describe('loaderHandler', () => { expect(requestEv.send).not.toHaveBeenCalled(); }); - it('sends a ServerError from the loader with Cache-Control: no-store over the loader option', async () => { + it('sends an HttpError from the loader with Cache-Control: no-store over the loader option', async () => { const requestEv = createRequestEv(); const loader = { __id: 'loader-id', __qrl: { call: vi.fn(async () => { - throw new ServerError(404, 'No such product'); + throw new HttpError(404, 'No such product'); }), }, __cacheControl: 60, @@ -305,14 +305,14 @@ describe('loaderHandler', () => { expect(requestEv.headers.get('Cache-Control')).toBe('no-store'); }); - it('sends a ServerError from the loader with Cache-Control: no-store over a header set by middleware', async () => { + it('sends an HttpError from the loader with Cache-Control: no-store over a header set by middleware', async () => { const requestEv = createRequestEv(); requestEv.headers.set('Cache-Control', 'public, max-age=60'); const loader = { __id: 'loader-id', __qrl: { call: vi.fn(async () => { - throw new ServerError(404, 'No such product'); + throw new HttpError(404, 'No such product'); }), }, }; diff --git a/packages/qwik-router/src/middleware/request-handler/server-error.ts b/packages/qwik-router/src/middleware/request-handler/http-error.ts similarity index 56% rename from packages/qwik-router/src/middleware/request-handler/server-error.ts rename to packages/qwik-router/src/middleware/request-handler/http-error.ts index e500cd526ce..1b3d1367cf5 100644 --- a/packages/qwik-router/src/middleware/request-handler/server-error.ts +++ b/packages/qwik-router/src/middleware/request-handler/http-error.ts @@ -2,7 +2,7 @@ import { AbortMessage } from './redirect-handler'; import { shareClassIdentity } from './shared-class-identity'; /** @public */ -export class ServerError extends Error { +export class HttpError extends Error { constructor( public status: number, public data: T @@ -10,14 +10,29 @@ export class ServerError extends Error { super(typeof data === 'string' ? data : undefined); } } -shareClassIdentity(ServerError, 'ServerError'); +shareClassIdentity(HttpError, 'HttpError'); + +/** + * The old name of `HttpError`. + * + * @deprecated Use `HttpError`. + * @public + */ +export const ServerError = HttpError; +/** + * The old name of `HttpError`. + * + * @deprecated Use `HttpError`. + * @public + */ +export type ServerError = HttpError; /** * `ev.redirect()`, `ev.error()`, etc. return a control-flow signal meant to be thrown. Throw it for * the user when they return it instead, so returning and throwing behave the same. */ export const throwIfControlFlowSignal = (value: unknown): void => { - if (value instanceof AbortMessage || value instanceof ServerError) { + if (value instanceof AbortMessage || value instanceof HttpError) { throw value; } }; diff --git a/packages/qwik-router/src/middleware/request-handler/server-error.unit.ts b/packages/qwik-router/src/middleware/request-handler/http-error.unit.ts similarity index 68% rename from packages/qwik-router/src/middleware/request-handler/server-error.unit.ts rename to packages/qwik-router/src/middleware/request-handler/http-error.unit.ts index d5e72885232..cb5cbd8f4bd 100644 --- a/packages/qwik-router/src/middleware/request-handler/server-error.unit.ts +++ b/packages/qwik-router/src/middleware/request-handler/http-error.unit.ts @@ -1,14 +1,21 @@ import { describe, expect, it } from 'vitest'; import { RedirectMessage } from './redirect-handler'; import { RewriteMessage } from './rewrite-handler'; -import { ServerError, throwIfControlFlowSignal } from './server-error'; +import { HttpError, ServerError, throwIfControlFlowSignal } from './http-error'; + +describe('ServerError', () => { + it('is the deprecated name of HttpError', () => { + expect(new ServerError(404, 'nope')).toBeInstanceOf(HttpError); + expect(new HttpError(404, 'nope')).toBeInstanceOf(ServerError); + }); +}); describe('throwIfControlFlowSignal', () => { it('throws control-flow signals so returning behaves like throwing', () => { const redirect = new RedirectMessage(); expect(() => throwIfControlFlowSignal(redirect)).toThrow(redirect); expect(() => throwIfControlFlowSignal(new RewriteMessage('/x'))).toThrow(RewriteMessage); - const error = new ServerError(404, 'nope'); + const error = new HttpError(404, 'nope'); expect(() => throwIfControlFlowSignal(error)).toThrow(error); }); diff --git a/packages/qwik-router/src/middleware/request-handler/index.ts b/packages/qwik-router/src/middleware/request-handler/index.ts index e7c00d75db5..579e0b3f20b 100644 --- a/packages/qwik-router/src/middleware/request-handler/index.ts +++ b/packages/qwik-router/src/middleware/request-handler/index.ts @@ -6,7 +6,7 @@ export { isStaticPath } from './static-paths'; export { mergeHeadersCookies } from './cookie'; -export { ServerError } from './server-error'; +export { HttpError, ServerError } from './http-error'; export { AbortMessage, RedirectMessage } from './redirect-handler'; export { RewriteMessage } from './rewrite-handler'; diff --git a/packages/qwik-router/src/middleware/request-handler/middleware.request-handler.api.md b/packages/qwik-router/src/middleware/request-handler/middleware.request-handler.api.md index c460f004c4b..70cbf8cdd1e 100644 --- a/packages/qwik-router/src/middleware/request-handler/middleware.request-handler.api.md +++ b/packages/qwik-router/src/middleware/request-handler/middleware.request-handler.api.md @@ -91,6 +91,15 @@ export interface EnvGetter { // @public (undocumented) export function getErrorHtml(status: number, e: any): string; +// @public (undocumented) +export class HttpError extends Error { + constructor(status: number, data: T); + // (undocumented) + data: T; + // (undocumented) + status: number; +} + // @public export type InternalRequest = false | 'loader' | 'action'; @@ -145,11 +154,13 @@ export interface RequestEventBase { // @public (undocumented) export interface RequestEventCommon extends RequestEventBase { - // Warning: (ae-forgotten-export) The symbol "ErrorCodes" needs to be exported by the entry point index.d.ts - readonly error: (statusCode: ErrorCodes, message: T) => ServerError; + // @deprecated + readonly error: (statusCode: ErrorCodes, message: T) => HttpError; // (undocumented) readonly exit: () => AbortMessage; readonly html: (statusCode: StatusCodes, html: string) => AbortMessage; + // Warning: (ae-forgotten-export) The symbol "ErrorCodes" needs to be exported by the entry point index.d.ts + readonly httpError: (statusCode: ErrorCodes, message: T) => HttpError; readonly json: (statusCode: StatusCodes, data: any) => AbortMessage; readonly locale: (local?: string) => string; // Warning: (ae-forgotten-export) The symbol "RedirectCode" needs to be exported by the entry point index.d.ts @@ -171,7 +182,7 @@ export interface RequestEventLoader extends Reque } // @public (undocumented) -export type RequestHandler = (ev: RequestEvent) => Promise | void | AbortMessage | ServerError; +export type RequestHandler = (ev: RequestEvent) => Promise | void | AbortMessage | HttpError; // Warning: (ae-forgotten-export) The symbol "QwikRouterRun" needs to be exported by the entry point index.d.ts // @@ -201,14 +212,11 @@ export class RewriteMessage extends AbortMessage { readonly pathname: string; } -// @public (undocumented) -export class ServerError extends Error { - constructor(status: number, data: T); - // (undocumented) - data: T; - // (undocumented) - status: number; -} +// @public @deprecated +export const ServerError: typeof HttpError; + +// @public @deprecated +export type ServerError = HttpError; // @public (undocumented) export interface ServerRenderOptions extends RenderOptions { diff --git a/packages/qwik-router/src/middleware/request-handler/render-cache-control.unit.ts b/packages/qwik-router/src/middleware/request-handler/render-cache-control.unit.ts index 91f0d95e02e..707c0063218 100644 --- a/packages/qwik-router/src/middleware/request-handler/render-cache-control.unit.ts +++ b/packages/qwik-router/src/middleware/request-handler/render-cache-control.unit.ts @@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest'; import { _setRouterConfig } from '../../runtime/src/router-config'; import { clearSsrCache } from './etag'; import { requestHandler } from './request-handler'; -import { ServerError } from './server-error'; +import { HttpError } from './http-error'; import type { ServerRequestEvent } from './types'; const routeState = { @@ -127,7 +127,7 @@ describe('render cache control', () => { const throwingThenErrorDocRender = vi .fn() .mockImplementationOnce(async () => { - throw new ServerError(500, 'render boom'); + throw new HttpError(500, 'render boom'); }) .mockImplementation(async (opts: any) => { opts.onBeforeFirstFlush?.({ hasCaughtError: false }); @@ -227,7 +227,7 @@ describe('render cache control', () => { routeState.module = { default: () => null, onGet: () => { - throw new ServerError(500, 'boom'); + throw new HttpError(500, 'boom'); }, }; const { ev, captured } = createServerRequestEvent(); diff --git a/packages/qwik-router/src/middleware/request-handler/request-body-limit.ts b/packages/qwik-router/src/middleware/request-handler/request-body-limit.ts index 6e22af463db..e2faabd3893 100644 --- a/packages/qwik-router/src/middleware/request-handler/request-body-limit.ts +++ b/packages/qwik-router/src/middleware/request-handler/request-body-limit.ts @@ -1,8 +1,8 @@ -import { ServerError } from './server-error'; +import { HttpError } from './http-error'; export const DEFAULT_REQUEST_BODY_LIMIT = 10 * 1024 * 1024; -export class RequestBodyLimitError extends ServerError { +export class RequestBodyLimitError extends HttpError { code = 'QWIK_REQUEST_BODY_LIMIT'; statusCode = 413; diff --git a/packages/qwik-router/src/middleware/request-handler/request-event-core.ts b/packages/qwik-router/src/middleware/request-handler/request-event-core.ts index 340652ef3a5..d9e51b7f5b7 100644 --- a/packages/qwik-router/src/middleware/request-handler/request-event-core.ts +++ b/packages/qwik-router/src/middleware/request-handler/request-event-core.ts @@ -22,11 +22,12 @@ import { } from './request-path'; import { AbortMessage, RedirectMessage } from './redirect-handler'; import { RewriteMessage } from './rewrite-handler'; -import { ServerError, throwIfControlFlowSignal } from './server-error'; +import { HttpError, throwIfControlFlowSignal } from './http-error'; import { encoder, getContentType } from './request-utils'; import type { CacheControl, CacheControlTarget, + ErrorCodes, RequestEvent, RequestEventCommon, RequestEventLoader, @@ -251,12 +252,15 @@ export function createRequestEvent( return locale || ''; }, - error: (statusCode: number, message: T) => { + httpError: (statusCode: number, message: T) => { status = statusCode; headers.delete('Cache-Control'); - return new ServerError(statusCode, message); + return new HttpError(statusCode, message); }, + error: (statusCode: ErrorCodes, message: T) => + requestEv.httpError(statusCode, message), + redirect: (statusCode: number, url: string) => { check(); status = statusCode; @@ -288,10 +292,7 @@ export function createRequestEvent( rewrite: (pathname: string) => { check(); if (pathname.startsWith('http')) { - throw new ServerError( - 400, - isDev ? 'Rewrite does not support absolute urls' : 'Bad Request' - ); + throw new HttpError(400, isDev ? 'Rewrite does not support absolute urls' : 'Bad Request'); } sharedMap.set(RequestEvIsRewrite, true); return exit(new RewriteMessage(pathname.replace(/\/+/g, '/'))); diff --git a/packages/qwik-router/src/middleware/request-handler/request-event.unit.ts b/packages/qwik-router/src/middleware/request-handler/request-event.unit.ts index 14679a2c02a..a6d3cb7340c 100644 --- a/packages/qwik-router/src/middleware/request-handler/request-event.unit.ts +++ b/packages/qwik-router/src/middleware/request-handler/request-event.unit.ts @@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest'; import { createRequestEvent } from './request-event-core'; import type { ServerRequestEvent } from './types'; import type { LoadedRoute } from '../../runtime/src/types'; -import { RedirectMessage, ServerError } from '@qwik.dev/router/middleware/request-handler'; +import { RedirectMessage, HttpError } from '@qwik.dev/router/middleware/request-handler'; function createMockServerRequestEvent( url = 'http://localhost:3000/test', @@ -245,12 +245,22 @@ describe('request-event redirect', () => { }).toThrow('Response already sent'); }); - it('should create public ServerError instances from requestEv.error()', () => { + it('should create public HttpError instances from requestEv.error()', () => { const requestEv = createMockRequestEvent(); const error = requestEv.error(418, 'teapot'); - expect(error).toBeInstanceOf(ServerError); + expect(error).toBeInstanceOf(HttpError); + expect(error.status).toBe(418); + expect(error.data).toBe('teapot'); + }); + + it('should return an HttpError with its status and message from requestEv.httpError()', () => { + const requestEv = createMockRequestEvent(); + + const error = requestEv.httpError(418, 'teapot'); + + expect(error).toBeInstanceOf(HttpError); expect(error.status).toBe(418); expect(error.data).toBe('teapot'); }); diff --git a/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers-core.ts b/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers-core.ts index 2514f08d922..2dec47d19a2 100644 --- a/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers-core.ts +++ b/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers-core.ts @@ -38,7 +38,7 @@ import { jsonRequestWrapper } from './handlers/json-request-wrapper'; import { actionHandler } from './handlers/action-handler'; import { IsQLoader, QLoaderId } from './request-path'; import { isStaticPath } from './static-paths'; -import type { ErrorCodes, RequestEvent, RequestEventBase, RequestHandler } from './types'; +import type { RequestEvent, RequestEventBase, RequestHandler } from './types'; import { QACTION_KEY, QFN_KEY } from '../../runtime/src/constants'; import { resolveRouteConfig } from '../../runtime/src/head'; import { @@ -52,7 +52,7 @@ import { import { HttpStatus } from './http-status-codes'; import { getQwikRouterServerData } from './response-page'; import { encoder, isContentType } from './request-utils'; -import { ServerError, throwIfControlFlowSignal } from './server-error'; +import { HttpError, throwIfControlFlowSignal } from './http-error'; const loadHttpError = () => import('../../runtime/src/http-error'); @@ -457,7 +457,7 @@ function createResolveRequestHandlers() { try { await requestEv.next(); } catch (e) { - if (!(e instanceof ServerError) || requestEv.headersSent) { + if (!(e instanceof HttpError) || requestEv.headersSent) { throw e; } @@ -541,7 +541,7 @@ function createResolveRequestHandlers() { | [args?: unknown[] | undefined, ...captured: unknown[]] | undefined; if (!Array.isArray(data) || !(Array.isArray(data[0]) || data[0] === undefined)) { - throw ev.error(500, 'Invalid request'); + throw new HttpError(500, 'Invalid request'); } const qrl = inlinedQrl(null, serverFnHash, data.slice(1)); let result: unknown; @@ -554,11 +554,11 @@ function createResolveRequestHandlers() { result = await (qrl as Function).apply(ev, data[0]); } } catch (err) { - if (err instanceof ServerError) { - throw ev.error(err.status as ErrorCodes, err.data); + if (err instanceof HttpError) { + throw err; } console.error(`Server function ${serverFnHash} failed:`, err); - throw ev.error(500, 'Invalid request'); + throw new HttpError(500, 'Invalid request'); } throwIfControlFlowSignal(result); if (isAsyncIterator(result)) { @@ -711,7 +711,7 @@ function createResolveRequestHandlers() { } if (forbidden) { - throw requestEv.error( + throw new HttpError( 403, `CSRF check failed. Cross-site ${requestEv.method} form submissions are forbidden. The request origin "${inputOrigin}" does not match the server origin "${origin}".` diff --git a/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.unit.ts b/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.unit.ts index 2f436735c66..64a756ad878 100644 --- a/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.unit.ts +++ b/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.unit.ts @@ -13,7 +13,7 @@ import { isContentType } from './request-utils'; import type { RequestEvent, ServerRequestEvent } from './types'; import { checkCSRF } from './resolve-request-handlers-core'; import type { LoadedRoute, RouteModule } from '../../runtime/src/types'; -import { ServerError } from '@qwik.dev/router/middleware/request-handler'; +import { HttpError } from '@qwik.dev/router/middleware/request-handler'; import { getLoaderName, IsQLoader, QLoaderId } from './request-path'; import { getRouteLoaderValues } from '../../runtime/src/route-loaders'; import { loadRoute } from '../../runtime/src/routing'; @@ -194,13 +194,12 @@ describe('resolve-request-handler', () => { 'Content-Type': 'application/qwik-json', }, }), - error: vi.fn((status: number, message: string) => ({ status, message })), exit: vi.fn(), parseBody: vi.fn(async () => [[]]), headers: new Headers(), } as any; - await expect(handlers[2](ev)).rejects.toEqual({ status: 500, message: 'Invalid request' }); + await expect(handlers[2](ev)).rejects.toEqual(new HttpError(500, 'Invalid request')); expect(ev.exit).toHaveBeenCalledTimes(1); expect(routeOnRequest).not.toHaveBeenCalled(); @@ -243,7 +242,6 @@ describe('resolve-request-handler', () => { }, url: new URL('http://server/path'), method: 'POST', - error: (status: number, msg: string) => new Error(msg), }; expect(() => checkCSRF(ev)).toThrow(/CSRF check failed/); @@ -259,7 +257,6 @@ describe('resolve-request-handler', () => { }, url: new URL('http://server/path'), method: 'POST', - error: (status: number, msg: string) => new Error(msg), }; expect(() => checkCSRF(ev)).toThrow(/CSRF check failed/); @@ -274,7 +271,6 @@ describe('resolve-request-handler', () => { }, url: new URL('http://server/path'), method: 'POST', - error: (status: number, msg: string) => new Error(msg), }; expect(() => checkCSRF(ev)).toThrow(/CSRF check failed/); @@ -290,7 +286,6 @@ describe('resolve-request-handler', () => { }, url: new URL('http://server/path'), method: 'POST', - error: (status: number, msg: string) => new Error(msg), }; // Should not throw @@ -307,7 +302,6 @@ describe('resolve-request-handler', () => { }, url: new URL('http://server/path'), method: 'POST', - error: (status: number, msg: string) => new Error(msg), }; // Should not throw @@ -324,7 +318,6 @@ describe('resolve-request-handler', () => { }, url: new URL('http://server/path'), method: 'POST', - error: (status: number, msg: string) => new Error(msg), }; // Should not throw @@ -341,7 +334,6 @@ describe('resolve-request-handler', () => { }, url: new URL('http://server/path'), method: 'POST', - error: (status: number, msg: string) => new Error(msg), }; expect(() => checkCSRF(ev)).toThrow(/CSRF check failed/); @@ -511,14 +503,14 @@ describe('resolve-request-handler', () => { }); describe('server error handling', () => { - it('should catch public ServerError instances in page middleware', async () => { + it('should catch public HttpError instances in page middleware', async () => { const route: LoadedRoute = { $routeName$: '/', $params$: {}, $mods$: [ { onRequest() { - throw new ServerError(418, 'teapot'); + throw new HttpError(418, 'teapot'); }, } as RouteModule, justHiModule as RouteModule, @@ -756,7 +748,7 @@ describe('resolve-request-handler', () => { it('errors the response when a blockSSR loader errors, before render', async () => { const route = pageRouteWithLoaders( makeLoader('l1', () => { - throw new ServerError(401, 'boom'); + throw new HttpError(401, 'boom'); }) ); const requestEv = runPage(route, exitRender()); @@ -767,11 +759,11 @@ describe('resolve-request-handler', () => { expect(requestEv.sharedMap.get(RequestEvHttpStatusMessage)).toBe('boom'); }); - it('clears resolved route loader data before rendering a ServerError page', async () => { + it('clears resolved route loader data before rendering an HttpError page', async () => { const route = pageRouteWithLoaders( makeLoader('protected', () => ({ secret: 'hidden' })), makeLoader('guard', () => { - throw new ServerError(401, 'login required'); + throw new HttpError(401, 'login required'); }) ); const renderHandler = vi.fn((requestEv: RequestEvent) => { @@ -790,10 +782,10 @@ describe('resolve-request-handler', () => { it('reports the first blockSSR loader (in route order) that errors', async () => { const route = pageRouteWithLoaders( makeLoader('first', () => { - throw new ServerError(401, 'first-error'); + throw new HttpError(401, 'first-error'); }), makeLoader('second', () => { - throw new ServerError(500, 'second-error'); + throw new HttpError(500, 'second-error'); }) ); const requestEv = runPage(route, exitRender()); @@ -809,7 +801,7 @@ describe('resolve-request-handler', () => { makeLoader( 'l1', () => { - throw new ServerError(401, 'boom'); + throw new HttpError(401, 'boom'); }, { blockSSR: false } ) diff --git a/packages/qwik-router/src/middleware/request-handler/shared-class-identity.unit.ts b/packages/qwik-router/src/middleware/request-handler/shared-class-identity.unit.ts index 01c3f291316..ac493f2a4c1 100644 --- a/packages/qwik-router/src/middleware/request-handler/shared-class-identity.unit.ts +++ b/packages/qwik-router/src/middleware/request-handler/shared-class-identity.unit.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; import { AbortMessage, RedirectMessage } from './redirect-handler'; import { RewriteMessage } from './rewrite-handler'; -import { ServerError } from './server-error'; +import { HttpError } from './http-error'; import { shareClassIdentity } from './shared-class-identity'; /** Declares the same classes again, as a second copy of the router does. */ @@ -10,33 +10,33 @@ const createRouterCopy = () => { shareClassIdentity(CopyAbortMessage, 'AbortMessage'); class CopyRedirectMessage extends CopyAbortMessage {} shareClassIdentity(CopyRedirectMessage, 'RedirectMessage'); - class CopyServerError extends Error {} - shareClassIdentity(CopyServerError, 'ServerError'); - return { CopyAbortMessage, CopyRedirectMessage, CopyServerError }; + class CopyHttpError extends Error {} + shareClassIdentity(CopyHttpError, 'HttpError'); + return { CopyAbortMessage, CopyRedirectMessage, CopyHttpError }; }; describe('router class identity across copies', () => { - const { CopyAbortMessage, CopyRedirectMessage, CopyServerError } = createRouterCopy(); + const { CopyAbortMessage, CopyRedirectMessage, CopyHttpError } = createRouterCopy(); it('recognizes instances made by another copy', () => { - expect(new CopyServerError() instanceof ServerError).toBe(true); + expect(new CopyHttpError() instanceof HttpError).toBe(true); expect(new CopyRedirectMessage() instanceof RedirectMessage).toBe(true); expect(new CopyRedirectMessage() instanceof AbortMessage).toBe(true); - expect(new ServerError(403, 'no') instanceof CopyServerError).toBe(true); + expect(new HttpError(403, 'no') instanceof CopyHttpError).toBe(true); }); it('keeps sibling and parent classes apart', () => { expect(new CopyAbortMessage() instanceof RedirectMessage).toBe(false); expect(new CopyRedirectMessage() instanceof RewriteMessage).toBe(false); - expect(new CopyServerError() instanceof AbortMessage).toBe(false); - expect(new Error('plain') instanceof ServerError).toBe(false); - expect(new ServerError(500, 'x') instanceof Error).toBe(true); + expect(new CopyHttpError() instanceof AbortMessage).toBe(false); + expect(new Error('plain') instanceof HttpError).toBe(false); + expect(new HttpError(500, 'x') instanceof Error).toBe(true); }); it('keeps user subclasses to their own instances', () => { - class NotFound extends ServerError {} - expect(new ServerError(500, 'x') instanceof NotFound).toBe(false); + class NotFound extends HttpError {} + expect(new HttpError(500, 'x') instanceof NotFound).toBe(false); expect(new NotFound(404, 'x') instanceof NotFound).toBe(true); - expect(new NotFound(404, 'x') instanceof ServerError).toBe(true); + expect(new NotFound(404, 'x') instanceof HttpError).toBe(true); }); }); diff --git a/packages/qwik-router/src/middleware/request-handler/types.ts b/packages/qwik-router/src/middleware/request-handler/types.ts index af1cc44de8d..0f6b59582c6 100644 --- a/packages/qwik-router/src/middleware/request-handler/types.ts +++ b/packages/qwik-router/src/middleware/request-handler/types.ts @@ -1,7 +1,7 @@ import type { _deserialize, _serialize, _verifySerializable } from '@qwik.dev/core/internal'; import type { Render, RenderOptions } from '@qwik.dev/core/server'; import type { Action, FailReturn, Loader } from '@qwik.dev/router'; -import type { ServerError } from './server-error'; +import type { HttpError } from './http-error'; import type { AbortMessage, RedirectMessage } from './redirect-handler'; import type { RewriteMessage } from './rewrite-handler'; @@ -72,7 +72,7 @@ export interface ServerRenderOptions extends RenderOptions { /** @public */ export type RequestHandler = ( ev: RequestEvent -) => Promise | void | AbortMessage | ServerError; +) => Promise | void | AbortMessage | HttpError; /** * Internal JSON request kind handled by Qwik Router, or `false` for normal page requests. @@ -228,11 +228,19 @@ export interface RequestEventCommon< readonly rewrite: (pathname: string) => RewriteMessage; /** - * When called, the response will immediately end with the given status code. This could be useful - * to end a response with `404`, and use the 404 handler in the routes directory. See - * https://developer.mozilla.org/en-US/docs/Web/HTTP/Status for which status code should be used. + * Returns an `HttpError` to throw, like `new HttpError(statusCode, message)`. Thrown from + * middleware or a blocking loader, it ends the response with that status and renders the nearest + * `error.tsx`. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status for which status code + * should be used. */ - readonly error: (statusCode: ErrorCodes, message: T) => ServerError; + readonly httpError: (statusCode: ErrorCodes, message: T) => HttpError; + + /** + * The old name of `httpError()`. + * + * @deprecated Use `httpError()` instead. + */ + readonly error: (statusCode: ErrorCodes, message: T) => HttpError; /** * Convenience method to send an text body response. The response will be automatically set the diff --git a/packages/qwik-router/src/middleware/request-handler/user-response.ts b/packages/qwik-router/src/middleware/request-handler/user-response.ts index 0e5ffd4a0ca..8b87bc4c2e0 100644 --- a/packages/qwik-router/src/middleware/request-handler/user-response.ts +++ b/packages/qwik-router/src/middleware/request-handler/user-response.ts @@ -11,7 +11,7 @@ import { getErrorHtml } from './error-handler'; import { createRequestEvent, type RequestEventInternal } from './request-event-core'; import { AbortMessage, RedirectMessage } from './redirect-handler'; import { RewriteMessage } from './rewrite-handler'; -import { ServerError } from './server-error'; +import { HttpError } from './http-error'; import { encoder } from './request-utils'; import type { ServerRequestEvent, StatusCodes } from './types'; @@ -77,7 +77,7 @@ async function runNext( const message = 'Resource Not Found'; requestEv.status(status); requestEv.html(status, getErrorHtml(status, message)); - return new ServerError(status, message); + return new HttpError(status, message); } let rewriteAttempt = 1; @@ -103,7 +103,7 @@ async function runNext( return await runOnce(); } else if (e instanceof AbortMessage) { return; - } else if (e instanceof ServerError && !requestEv.headersSent) { + } else if (e instanceof HttpError && !requestEv.headersSent) { const status = e.status as StatusCodes; requestEv.headers.set('Cache-Control', 'no-store'); const accept = requestEv.request.headers.get('Accept'); diff --git a/packages/qwik-router/src/middleware/request-handler/user-response.unit.ts b/packages/qwik-router/src/middleware/request-handler/user-response.unit.ts index f56cdd47a2b..71508a2dc4b 100644 --- a/packages/qwik-router/src/middleware/request-handler/user-response.unit.ts +++ b/packages/qwik-router/src/middleware/request-handler/user-response.unit.ts @@ -15,7 +15,7 @@ vi.mock('./async-request-store', () => ({ }, })); -import { ServerError } from './server-error'; +import { HttpError } from './http-error'; import { runQwikRouter } from './user-response'; import type { ServerRequestEvent } from './types'; @@ -67,7 +67,7 @@ describe('runQwikRouter', () => { }); it.each(['application/json', 'text/html'])( - 'sends a thrown ServerError with Cache-Control: no-store for %s', + 'sends a thrown HttpError with Cache-Control: no-store for %s', async (accept) => { const requestEv = { originalUrl: new URL('http://localhost/members/'), @@ -75,7 +75,7 @@ describe('runQwikRouter', () => { headers: new Headers({ 'Cache-Control': 'public, max-age=60' }), headersSent: false, next: vi.fn(async () => { - throw new ServerError(403, 'Members only'); + throw new HttpError(403, 'Members only'); }), send: vi.fn(), html: vi.fn(), diff --git a/packages/qwik-router/src/runtime/src/qwik-router.runtime.api.md b/packages/qwik-router/src/runtime/src/qwik-router.runtime.api.md index 4eb41b18c74..03ea7a4eaa8 100644 --- a/packages/qwik-router/src/runtime/src/qwik-router.runtime.api.md +++ b/packages/qwik-router/src/runtime/src/qwik-router.runtime.api.md @@ -14,6 +14,7 @@ import { CookieValue } from '@qwik.dev/router/middleware/request-handler'; import { DeferReturn } from '@qwik.dev/router/middleware/request-handler'; import type { EnvGetter } from '@qwik.dev/router/middleware/request-handler'; import { EventHandler } from '@qwik.dev/core'; +import type { HttpError } from '@qwik.dev/router/middleware/request-handler'; import { InternalRequest } from '@qwik.dev/router/middleware/request-handler'; import { JSXOutput } from '@qwik.dev/core'; import { NoSerialize } from '@qwik.dev/core'; @@ -31,7 +32,6 @@ import { RequestEventLoader } from '@qwik.dev/router/middleware/request-handler' import { RequestHandler } from '@qwik.dev/router/middleware/request-handler'; import type { ResolveSyncValue } from '@qwik.dev/router/middleware/request-handler'; import type { SerializationStrategy } from '@qwik.dev/core/internal'; -import type { ServerError } from '@qwik.dev/router/middleware/request-handler'; import * as v from 'valibot'; import { ValueOrPromise } from '@qwik.dev/core'; import { ValueOrPromise as ValueOrPromise_2 } from '@qwik.dev/core/internal'; @@ -199,7 +199,7 @@ export type Editable = { }; // @public -export type ExcludeControlFlow = Exclude; +export type ExcludeControlFlow = Exclude; // @public (undocumented) export type FailOfRest = REST extends readonly DataValidator[] ? ERROR : never; diff --git a/packages/qwik-router/src/runtime/src/route-loaders.ts b/packages/qwik-router/src/runtime/src/route-loaders.ts index e2908ec043f..973fadb401f 100644 --- a/packages/qwik-router/src/runtime/src/route-loaders.ts +++ b/packages/qwik-router/src/runtime/src/route-loaders.ts @@ -29,10 +29,7 @@ import type { import { _asyncRequestStore } from '../../middleware/request-handler/async-request-store'; import { getLoaderName } from '../../middleware/request-handler/request-path'; import { RedirectMessage } from '../../middleware/request-handler/redirect-handler'; -import { - ServerError, - throwIfControlFlowSignal, -} from '../../middleware/request-handler/server-error'; +import { HttpError, throwIfControlFlowSignal } from '../../middleware/request-handler/http-error'; import { ensureSlash } from '../../utils/pathname'; import { DEFAULT_LOADERS_SERIALIZATION_STRATEGY } from './constants'; import { RouteLoaderCtxContext, RouteStateContext } from './contexts'; @@ -78,12 +75,12 @@ export const ROUTE_PATH_HEADER = 'X-Qwik-route-path'; * * - `d` — data: the loader's return value (including a `fail()` result, which is plain data) * - `r` — redirect: URL to navigate to (from `throw redirect()`) - * - `e` — error: a ServerError (from a thrown `ServerError` / `error()`) + * - `e` — error: an HttpError (from a thrown `HttpError` / `error()`) */ export type LoaderResponse = { d?: unknown; r?: string; - e?: InstanceType; + e?: InstanceType; }; /** @@ -1013,6 +1010,10 @@ export const getLoaderRequestEvent = ( const detachResponseFromEvent = (requestEv: RequestEvent): RequestEvent => { let status = 200; const headers = new Headers(); + const httpError = (statusCode: number, message: unknown) => { + status = statusCode; + return new HttpError(statusCode, message); + }; return Object.create(requestEv, { headers: { value: headers, enumerable: true }, status: { @@ -1024,13 +1025,8 @@ const detachResponseFromEvent = (requestEv: RequestEvent): RequestEvent => { }, enumerable: true, }, - error: { - value: (statusCode: number, message: unknown) => { - status = statusCode; - return new ServerError(statusCode, message); - }, - enumerable: true, - }, + httpError: { value: httpError, enumerable: true }, + error: { value: httpError, enumerable: true }, redirect: { value: (statusCode: number, url: string) => { status = statusCode; @@ -1080,7 +1076,7 @@ export const getRouteLoaderResponse = async ( requestEv.headers.delete('Location'); return { r: location }; } - if (err instanceof ServerError) { + if (err instanceof HttpError) { return { e: err }; } throw err; diff --git a/packages/qwik-router/src/runtime/src/route-loaders.unit.ts b/packages/qwik-router/src/runtime/src/route-loaders.unit.ts index 03fc7c4fbce..a68f4a3639b 100644 --- a/packages/qwik-router/src/runtime/src/route-loaders.unit.ts +++ b/packages/qwik-router/src/runtime/src/route-loaders.unit.ts @@ -15,7 +15,7 @@ import { type RouteLoaderCtx, } from './route-loaders'; import { loadRoute } from './routing'; -import { ServerError } from '../../middleware/request-handler/server-error'; +import { HttpError } from '../../middleware/request-handler/http-error'; import type { LoaderInternal, RouteModule } from './types'; describe('route loader execution', () => { @@ -314,15 +314,15 @@ describe('getRouteLoaderResponse envelope', () => { expect(response.e).toBeUndefined(); }); - it('routes a thrown ServerError to the error channel', async () => { + it('routes a thrown HttpError to the error channel', async () => { const qrl = createQrl('error-loader', async () => { - throw new ServerError(500, 'boom'); + throw new HttpError(500, 'boom'); }); const response = await getRouteLoaderResponse(qrl, undefined, requestEv); expect(response.d).toBeUndefined(); - expect(response.e).toBeInstanceOf(ServerError); + expect(response.e).toBeInstanceOf(HttpError); expect(response.e?.status).toBe(500); }); }); diff --git a/packages/qwik-router/src/runtime/src/types.ts b/packages/qwik-router/src/runtime/src/types.ts index 36c5a5d3d34..0774fb4ab3a 100644 --- a/packages/qwik-router/src/runtime/src/types.ts +++ b/packages/qwik-router/src/runtime/src/types.ts @@ -18,7 +18,7 @@ import type { RequestEventLoader, RequestHandler, ResolveSyncValue, - ServerError, + HttpError, } from '@qwik.dev/router/middleware/request-handler'; import type * as v from 'valibot'; import type * as z from 'zod'; @@ -455,7 +455,7 @@ export interface LoadedRoute { $routeBundleNames$?: string[] | undefined; /** Whether this route is a not-found (404) route */ $notFound$?: boolean; - /** The nearest _E (error.tsx) boundary's chain to render on a thrown ServerError (in its layouts). */ + /** The nearest _E (error.tsx) boundary's chain to render on a thrown HttpError (in its layouts). */ $errorLoader$?: ModuleLoader[]; /** Merged array of routeLoader$ hashes from all matched nodes (layouts + page) */ $loaders$?: string[]; @@ -1016,7 +1016,7 @@ export type FailReturn = T & Failed; * * @public */ -export type ExcludeControlFlow = Exclude; +export type ExcludeControlFlow = Exclude; /** @public */ export interface LoaderSignal extends ComputedSignal< diff --git a/starters/features/builder.io/src/routes/[...index]/index.tsx b/starters/features/builder.io/src/routes/[...index]/index.tsx index e5451f0f9b1..2b34d1726bc 100644 --- a/starters/features/builder.io/src/routes/[...index]/index.tsx +++ b/starters/features/builder.io/src/routes/[...index]/index.tsx @@ -1,6 +1,7 @@ import { Content, fetchOneEntry } from "@builder.io/sdk-qwik"; import { component$ } from "@qwik.dev/core"; import { routeLoader$ } from "@qwik.dev/router"; +import { HttpError } from "@qwik.dev/router/middleware/request-handler"; import { CUSTOM_COMPONENTS } from "~/components/builder-registry"; export const BUILDER_MODEL = "page"; @@ -21,7 +22,7 @@ export const useBuilderContent = routeLoader$(async (event) => { // If there's no content, throw a 404. // You can use your own 404 component here if (!builderContent && !isPreviewing) { - throw event.error(404, "Page not found"); + throw new HttpError(404, "Page not found"); } // return content fetched from Builder, which is JSON return builderContent;