import { createRequestEvent } from "./event.mjs"; import { joinURL, withQuery } from "ufo"; import { appendVary, describeError, isExpectedError, isJsonRequest } from "nuxt/internal/renderer/error"; import { mergeHeaders } from "nuxt/internal/renderer/headers"; import { useServerHooks } from "nuxt/server"; import { buildAssetsURL, publicAssetsURL } from "#internal/nuxt/paths"; //#region src/runtime/renderer.ts /** An error carrying the HTTP status the renderer refused a request with. */ var NuxtServerError = class extends Error { status; statusText; data; headers; constructor(init) { super(init.statusText || `Request failed with status ${init.status}`); this.name = "NuxtServerError"; this.status = init.status; this.statusText = init.statusText || ""; this.data = init.data; this.headers = init.headers; } }; /** * The capabilities `@nuxt/vite-server` provides to the renderer. Everything comes from the * platform or from values the build serialised, so the same options run on a node server * and in a web-standard worker. * * Route rules come from the matcher the build compiled, so a route is server-rendered * unless a rule says otherwise. */ function createRendererOptions(runtimeConfig, matchRouteRules, prerender) { const devErrors = import.meta.dev ? import("./dev-error.mjs") : void 0; return { runtimeConfig, buildAssetsURL, publicAssetsURL, getRouteRules: (event) => ({ ssr: true, ...matchRouteRules(event.url.pathname) }), hooks: useServerHooks, createResponse: (body, init) => new Response(body, init), createError: (init) => new NuxtServerError(init), prerender, onRenderSuccess: devErrors ? () => { devErrors.then(({ clearErrorReport }) => clearErrorReport()).catch(() => {}); } : void 0, captureError: (error) => { if (!import.meta.dev) console.error(error); }, onDevError: devErrors ? (error, event, options) => devErrors.then(({ observeDevError }) => observeDevError(error, event.req, options)) : void 0 }; } /** * Header the crawler reads additional routes from. The renderer collects them on the * request event, which does not cross the handler boundary, so they ride the response. */ const PRERENDER_HINTS_HEADER = "x-nuxt-prerender"; /** * A web-standard handler over the renderer: it renders the request, and renders the app's * error page for a request the render refused. In development it also serves the live * error channel and publishes what it failed on to it. */ function createFetchHandler(renderer, matchRouteRules) { return async function fetch(request) { if (import.meta.dev) { const devErrors = await import("./dev-error.mjs"); if (devErrors.isErrorChannelRequest(new URL(request.url).pathname)) return devErrors.fetchErrorChannel(request); } const event = createRequestEvent(request); const rules = matchRouteRules(event.url.pathname); if (rules.redirect) return redirectResponse(event, rules.redirect, rules.headers); try { const response = await renderer.fetch(event); applyHeaders(response, rules.headers); if (import.meta.prerender) applyPrerenderHints(event, response); return response; } catch (error) { const response = await renderError(renderer, request, error, event); applyHeaders(response, rules.headers); if (import.meta.prerender) applyPrerenderHints(event, response); return response; } }; } function applyHeaders(response, headers) { for (const name in headers) response.headers.set(name, headers[name]); } /** * Answer a `redirect` rule. A target carrying `**` moves a whole subtree: the tail of the * request past the prefix the rule matched under is interpolated into it. Targets naming a * parameter of the matched pattern are not resolved; those need a server runtime. */ function redirectResponse(event, redirect, headers) { let location = redirect.to; if (location.includes("**")) { const path = event.url.pathname; const tail = redirect.base && path.startsWith(redirect.base) ? path.slice(redirect.base.length) : path; location = location.endsWith("/**") ? joinURL(location.slice(0, -3), tail) : location.replace("**", tail.replace(/^\//, "")); } const target = appendSearch(location, event.url.search); const response = new Response(redirectBody(target), { status: redirect.status ?? 307, headers: { "location": target, "content-type": "text/html" } }); applyHeaders(response, headers); return response; } /** A meta-refresh document, so a prerendered redirect is followed when served as a static file. */ function redirectBody(location) { return ``; } const REDIRECT_ESCAPES = { "\"": "%22", "'": "%27", "<": "%3C", ">": "%3E", "&": "%26" }; const REDIRECT_UNSAFE_RE = /["'<>&]/g; /** Carry the request's query onto a redirect target, ahead of any fragment the target names. */ function appendSearch(target, search) { if (!search) return target; const hashIndex = target.indexOf("#"); const path = hashIndex === -1 ? target : target.slice(0, hashIndex); const hash = hashIndex === -1 ? "" : target.slice(hashIndex); return path + (!path.includes("?") ? "?" : path.endsWith("?") || path.endsWith("&") ? "" : "&") + search.slice(1) + hash; } function applyPrerenderHints(event, response) { const paths = event.context.nuxt?.prerenderRoutes; if (!paths?.length) return; response.headers.append(PRERENDER_HINTS_HEADER, paths.map((path) => encodeURIComponent(path)).join(", ")); } async function renderError(renderer, request, error, event) { const described = describeError(error); const { status, statusText, message } = described; const url = new URL(request.url); const devErrors = import.meta.dev ? await import("./dev-error.mjs") : void 0; const report = devErrors ? await devErrors.observeDevError(error, request, { expected: isExpectedError(error, described) }).catch(() => void 0) : void 0; if (isJsonRequest(request, url.pathname)) { const body = { error: true, status, statusText, message, data: described.data, ...import.meta.dev && { stack: error?.stack?.split("\n").map((line) => line.trim()) } }; return new Response(JSON.stringify(body), { status, statusText, headers: errorResponseHeaders(event, described, "application/json;charset=utf-8") }); } const data = error?.data; const stack = error?.stack; const errorEvent = createRequestEvent(new Request(withQuery(new URL("/__nuxt_error", url).href, { status: String(status), statusCode: String(status), statusText, statusMessage: statusText, message, url: url.pathname + url.search + url.hash, ...data === void 0 ? {} : { data: typeof data === "string" ? data : JSON.stringify(data) }, ...import.meta.dev && stack ? { stack } : {} }), { headers: request.headers })); const state = (import.meta.prerender ? event.context.nuxt : void 0) || {}; state["~rendering-error"] = true; if (devErrors) { const cause = devErrors.errorCause(error); if (cause !== void 0) state["~error-cause"] = cause; } errorEvent.context.nuxt = state; if (import.meta.prerender) event.context.nuxt = state; const rendered = await renderer.fetch(errorEvent).catch(() => null); if (rendered) { const responseHeaders = errorResponseHeaders(event, described, "text/html;charset=utf-8", rendered.headers); if (report && !import.meta.test) { const html = await rendered.text(); const body = await report.overlay(html).catch(() => html); return new Response(body, { status, statusText, headers: responseHeaders }); } return new Response(rendered.body, { status, statusText, headers: responseHeaders }); } if (report) { const page = await report.page().catch(() => void 0); if (page) return new Response(page, { status, statusText, headers: errorResponseHeaders(event, described, "text/html;charset=utf-8") }); } return new Response(message, { status, statusText, headers: errorResponseHeaders(event, described, "text/plain;charset=utf-8") }); } function errorResponseHeaders(event, described, contentType, page) { const headers = new Headers(event.res.headers); if (page) mergeHeaders(headers, page); mergeHeaders(headers, described.headers); headers.set("content-type", contentType); appendVary(headers, "accept, sec-fetch-mode"); return headers; } //#endregion export { NuxtServerError, createFetchHandler, createRendererOptions };