UNPKG

astro

Version:

Astro is a modern site builder with web best practices, performance, and DX front-of-mind.

189 lines (188 loc) • 8.23 kB
import { removeTrailingForwardSlash } from "@astrojs/internal-helpers/path"; import { shouldAppendForwardSlash } from "../core/build/util.js"; import { clientLocalsSymbol } from "../core/constants.js"; import { getEnvironment, setEnvironment } from "../core/environment/index.js"; import { createSafeError } from "../core/errors/index.js"; import { setLogger } from "../core/logger/manifest-logger.js"; import { createRequest } from "../core/request.js"; import { validateAndDecodePathname } from "../core/util/pathname.js"; import { SERIALIZED_MANIFEST_ID } from "../manifest/serialized.js"; import { recordServerError } from "../vite-plugin-astro-server/error.js"; import { runWithErrorHandling } from "../vite-plugin-astro-server/index.js"; import { handle500Response, writeSSRResult } from "../vite-plugin-astro-server/response.js"; /** * The manifest object most recently evaluated by each environment's module * runner. In dev the manifest module is invalidated whenever a src file * changes, so re-evaluations produce NEW manifest objects that back * `new FetchState(request)` inside the fetchable graph (ambient resolution). * Composition (environment record + logger) is re-registered for each new * object so those states behave like this environment's app. Keyed by the * ModuleLoader because each runnable environment (ssr, prerender) has its own * loader, graph, and manifest instance. */ const runnerManifests = new WeakMap(); /** * Loads the user's `src/fetch.ts` (via `virtual:astro:fetchable`) and sets it * as the fetch handler. Called on every request so that HMR invalidation of * the virtual module is picked up automatically. Vite caches the module * internally so repeated calls are cheap. */ async function loadFetchHandler(app, loader) { try { // Keep the runner-graph manifest's composition registered (see // `runnerManifests`). Cheap when nothing was invalidated: one cached // module lookup and an identity check. const { manifest } = await loader.import(SERIALIZED_MANIFEST_ID); if (manifest && manifest !== runnerManifests.get(loader) && manifest !== app.manifest) { setEnvironment(manifest, getEnvironment(app.manifest)); setLogger(manifest, app.logger); runnerManifests.set(loader, manifest); } } catch {} try { const mod = await loader.import("virtual:astro:fetchable"); if (mod?.default && !mod.isDefaultFetchHandler) { app.setFetchHandler(mod.default); } } catch {} } /** * Handle a dev-server request: the HTTP glue that drives the `DevFacadeApp` * the way an adapter does — it sits outside the functional core, like the * node adapter's `serve-app.ts`. The ModuleLoader and AstroSettings are * closed over at composition time in `createAstroServerApp` and passed as * `deps`. * * @returns Whether or not the request was handled by this handler. If the * result is not `true`, then the request has not been handled yet and other * handlers can be run. */ export async function handleDevRequest(app, deps, { incomingRequest, incomingResponse, isHttps, prerenderOnly }) { const { loader, settings, controller } = deps; const manifest = app.manifest; // Build a basic origin from the socket protocol and Host header. // X-Forwarded-* headers are resolved later inside FetchState, which // validates them against allowedDomains and updates the URL. This // lets user-provided fetch handlers (src/app.ts) set or modify // forwarded headers before FetchState picks them up. const protocol = isHttps ? "https" : "http"; const host = incomingRequest.headers[":authority"] ?? incomingRequest.headers.host; const origin = `${protocol}://${host}`; const url = new URL(origin + incomingRequest.url); let pathname; if (manifest.trailingSlash === "never" && !incomingRequest.url) { pathname = ""; } else { // We already have a middleware that checks if there's an incoming URL that has invalid URI, so it's safe // to only handle paths that exceed the supported decoding depth here. try { pathname = validateAndDecodePathname(url.pathname); } catch { pathname = decodeURI(url.pathname); } } // Add config.base back to url before passing it to SSR url.pathname = removeTrailingForwardSlash(manifest.base) + url.pathname; if (url.pathname.endsWith("/") && !shouldAppendForwardSlash(manifest.trailingSlash, manifest.buildFormat)) { url.pathname = url.pathname.slice(0, -1); } await loadFetchHandler(app, loader); // RouteCache is intentionally not cleared per request. devMatch() can use // getStaticPaths() to test dynamic route candidates before the later render // resolves props from the same static-path table. HMR/content invalidation // clears stale entries through module identity checks or content-change events. let handled = true; await runWithErrorHandling({ controller, pathname, async run() { const matchedRoute = await app.devMatch(pathname, { prerenderOnly }); if (!matchedRoute) { if (prerenderOnly) { // In prerender-only mode, signal that we didn't handle this // so the caller can fall through to the SSR handler. handled = false; return; } // This should never happen, because ensure404Route will add a 404 route if none exists. throw new Error("No route matched, and default 404 route was not found."); } // When running as the prerender handler, only handle prerendered routes. // If the best-matching route is SSR, let the SSR handler handle it instead. if (prerenderOnly && !matchedRoute.routeData.prerender) { handled = false; return; } // Delay reading the request body until prerenderOnly routing has decided // this handler really owns the request. Otherwise a prerender pass that // falls through to SSR would exhaust the body stream first. let body = undefined; if (!(incomingRequest.method === "GET" || incomingRequest.method === "HEAD")) { let bytes = []; await new Promise((resolve, reject) => { incomingRequest.on("data", (part) => { bytes.push(part); }); incomingRequest.on("end", resolve); // Without this, an errored stream (aborted upload, malformed // chunked encoding) never emits 'end' and the request hangs. incomingRequest.on("error", reject); }); body = Buffer.concat(bytes); } // Wire an AbortController to the socket so request.signal // reflects client disconnection, matching production behaviour. const abortController = new AbortController(); const socket = incomingRequest.socket; const onSocketClose = () => { if (!abortController.signal.aborted) { abortController.abort(); } }; if (socket.destroyed) { onSocketClose(); } else { socket.on("close", onSocketClose); } try { const request = createRequest({ url, headers: incomingRequest.headers, method: incomingRequest.method, body, logger: app.logger, isPrerendered: matchedRoute.routeData.prerender, routePattern: matchedRoute.routeData.component, init: { signal: abortController.signal } }); // This is required for adapters to set locals in dev mode. They use a dev server middleware to inject locals to the `http.IncomingRequest` object. const locals = Reflect.get(incomingRequest, clientLocalsSymbol); // Set user specified headers to response object. for (const [name, value] of Object.entries(settings.config.server.headers ?? {})) { if (value) incomingResponse.setHeader(name, value); } const clientAddress = incomingRequest.socket.remoteAddress; const response = await app.render(request, { locals, routeData: matchedRoute.routeData, clientAddress }); await writeSSRResult(request, response, incomingResponse); } finally { // Remove the per-request socket listener so it doesn't accumulate // across keep-alive requests that reuse the same socket. socket.off("close", onSocketClose); } }, onError(_err) { const error = createSafeError(_err); if (loader) { const { errorWithMetadata } = recordServerError(loader, manifest, app.logger, error); // Dev error overlay. handle500Response(loader, incomingResponse, errorWithMetadata); } return error; } }); return handled; }