diff --git a/.changeset/lucky-lies-exist.md b/.changeset/lucky-lies-exist.md new file mode 100644 index 00000000000..a08597622ca --- /dev/null +++ b/.changeset/lucky-lies-exist.md @@ -0,0 +1,5 @@ +--- +'@builder.io/qwik-city': patch +--- + +server$ functions now correctly throw 4xx errors on the client and server$ errors can be caught by @plugin middleware diff --git a/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts b/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts index 1ba0ef5b3f3..e7c3d8f64b0 100644 --- a/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts +++ b/packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts @@ -12,7 +12,6 @@ import type { RouteModule, ValidatorReturn, } from '../../runtime/src/types'; -import { ServerError } from './error-handler'; import { HttpStatus } from './http-status-codes'; import { RedirectMessage } from './redirect-handler'; import { @@ -305,23 +304,12 @@ async function pureServerFunction(ev: RequestEvent) { const [qrl, ...args] = data; if (isQrl(qrl) && qrl.getHash() === fn) { let result: unknown; - try { - if (isDev) { - result = await measure(ev, `server_${qrl.getSymbol()}`, () => - (qrl as Function).apply(ev, args) - ); - } else { - result = await (qrl as Function).apply(ev, args); - } - } catch (err) { - if (err instanceof ServerError) { - ev.headers.set('Content-Type', 'application/qwik-json'); - ev.send(err.status, await qwikSerializer._serializeData(err.data, true)); - return; - } - ev.headers.set('Content-Type', 'application/qwik-json'); - ev.send(500, await qwikSerializer._serializeData(err, true)); - return; + if (isDev) { + result = await measure(ev, `server_${qrl.getSymbol()}`, () => + (qrl as Function).apply(ev, args) + ); + } else { + result = await (qrl as Function).apply(ev, args); } if (isAsyncIterator(result)) { ev.headers.set('Content-Type', 'text/qwik-json-stream'); diff --git a/packages/qwik-city/src/middleware/request-handler/user-response.ts b/packages/qwik-city/src/middleware/request-handler/user-response.ts index 522286950c3..8a8d3431ced 100644 --- a/packages/qwik-city/src/middleware/request-handler/user-response.ts +++ b/packages/qwik-city/src/middleware/request-handler/user-response.ts @@ -1,7 +1,12 @@ import type { QwikSerializer, ServerRequestEvent, StatusCodes } from './types'; import type { RequestEvent, RequestHandler } from '@builder.io/qwik-city'; -import { createRequestEvent, getRequestMode, type RequestEventInternal } from './request-event'; -import { ErrorResponse, getErrorHtml, minimalHtmlResponse } from './error-handler'; +import { + RequestEvQwikSerializer, + createRequestEvent, + getRequestMode, + type RequestEventInternal, +} from './request-event'; +import { ErrorResponse, ServerError, getErrorHtml, minimalHtmlResponse } from './error-handler'; import { AbortMessage, RedirectMessage } from './redirect-handler'; import type { LoadedRoute } from '../../runtime/src/types'; import { encoder } from './resolve-request-handlers'; @@ -72,6 +77,13 @@ async function runNext(requestEv: RequestEventInternal, resolve: (value: any) => const status = e.status as StatusCodes; requestEv.html(status, html); } + } else if (e instanceof ServerError) { + if (!requestEv.headersSent) { + const status = e.status as StatusCodes; + const qwikSerializer = requestEv[RequestEvQwikSerializer]; + requestEv.headers.set('Content-Type', 'application/qwik-json'); + requestEv.send(status, await qwikSerializer._serializeData(e.data, true)); + } } else if (!(e instanceof AbortMessage)) { if (getRequestMode(requestEv) !== 'dev') { try { diff --git a/packages/qwik-city/src/runtime/src/server-functions.ts b/packages/qwik-city/src/runtime/src/server-functions.ts index 6a40fcca57d..c8731fdea21 100644 --- a/packages/qwik-city/src/runtime/src/server-functions.ts +++ b/packages/qwik-city/src/runtime/src/server-functions.ts @@ -479,19 +479,19 @@ export const serverQrl = ( } else if (contentType === 'application/qwik-json') { const str = await res.text(); const obj = await _deserializeData(str, ctxElm ?? document.documentElement); - if (res.status >= 500) { + if (res.status >= 400) { throw obj; } return obj; } else if (contentType === 'application/json') { const obj = await res.json(); - if (res.status >= 500) { + if (res.status >= 400) { throw obj; } return obj; } else if (contentType === 'text/plain' || contentType === 'text/html') { const str = await res.text(); - if (res.status >= 500) { + if (res.status >= 400) { throw str; } return str; diff --git a/starters/apps/qwikcity-test/src/routes/(common)/server-func/server-error/index.tsx b/starters/apps/qwikcity-test/src/routes/(common)/server-func/server-error/index.tsx index 9b20ebee7fa..1e7bae39e5b 100644 --- a/starters/apps/qwikcity-test/src/routes/(common)/server-func/server-error/index.tsx +++ b/starters/apps/qwikcity-test/src/routes/(common)/server-func/server-error/index.tsx @@ -3,14 +3,14 @@ import { server$ } from "@builder.io/qwik-city"; import { ServerError } from "@builder.io/qwik-city/middleware/request-handler"; import { delay } from "../../actions/login"; -type ResponseTuple = [null | string, string]; +type ErrorReason = { reason: string }; -const serverFunctionA = server$(async function a(): Promise { - throw new ServerError<[string]>(401, ["my error"]); +const serverFunctionA = server$(async function a(): Promise { + throw new ServerError(401, { reason: "my error" }); }); -const serverFunctionB = server$(async function b(): Promise { - return [null, this.method || ""]; +const serverFunctionB = server$(async function b(): Promise { + return this.method; }); export const MultipleServerFunctionsInvokedInTask = component$(() => { @@ -18,13 +18,17 @@ export const MultipleServerFunctionsInvokedInTask = component$(() => { const methodB = useSignal(""); useVisibleTask$(async () => { - const [error /*, data */] = await serverFunctionA(); - if (error) { - methodA.value = error; + try { + await serverFunctionA(); + } catch (err: any) { + if (isErrorReason(err)) { + methodA.value = err.reason; + } } + await delay(1); - // err, method - const [, method] = await serverFunctionB(); + + const method = await serverFunctionB(); methodB.value = method; }); @@ -43,3 +47,11 @@ export default component$(() => { ); }); + +function isErrorReason(err: any): err is ErrorReason { + if (typeof err.reason === "string") { + return true; + } + + return false; +}