UNPKG

astro

Version:

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

232 lines (231 loc) • 8.71 kB
import { handleAction } from "../../actions/handler.js"; import { REROUTABLE_STATUS_CODES } from "../constants.js"; import { handleTrailingSlash } from "./trailing-slash-handler.js"; import { rejectInvalidEncoding } from "./invalid-encoding.js"; import { handleCache, provideCache } from "../cache/handler.js"; import { getEnvironment } from "../environment/index.js"; import { renderErrorFromState } from "../errors/handler.js"; import { handleNotFound } from "../errors/not-found.js"; import { ALL_FETCH_FEATURES, markFeatureUsed, FetchFeatures } from "../fetch/features.js"; import { finalizeI18n, getI18n } from "../i18n/handler.js"; import { getResolvedLogger } from "../logger/manifest-logger.js"; import { handleMiddleware } from "../middleware/astro-middleware.js"; import { handlePages } from "../pages/handler.js"; import { renderRedirect } from "../redirects/render.js"; import { redirectDelayFor, renderRedirectPage } from "./redirect-page.js"; import { provideSession } from "../session/provider.js"; import { prepareResponse } from "../app/prepare-response.js"; import { getDefaultStatusCode } from "./helpers.js"; /** * Dispatches request logging: through the facade's late-bound * `logThisRequest` hook when the state was built by `BaseApp.render`'s fast * path (preserving subclass overrides), else through the environment's * `logRequest` behavior (dev request lines; prod/build/container no-op). */ function logRequestFromState(state, payload) { if (state.logRequest) { state.logRequest(payload); } else { getEnvironment(state.manifest).logRequest(state.manifest, payload); } } /** * Runs actions then pages — the callback at the bottom of the middleware * chain. A module-level function so passing it by reference costs zero * per-request allocation. */ function actionsAndPages(state, ctx) { if (!state.skipMiddleware) { const actionResult = handleAction(ctx, state); if (actionResult) { return actionResult.then((response) => response ?? handlePages(state, ctx)); } } return handlePages(state, ctx); } /** * Swaps a bodiless redirect response for one carrying the project's rendered * `src/pages/3xx.astro`. * * Only the build environment opts in (see `RenderEnvironment.rendersRedirectPage`): * a static site has no `Location` header to follow, so the HTML body is the * redirect. Responses that already carry a body — the trailing-slash * normalizer's, or a page that rendered its own — are left alone. */ async function withRedirectPageBody(state, response) { const status = response.status; if (status < 300 || status >= 400 || response.body !== null) { return response; } if (!getEnvironment(state.manifest).rendersRedirectPage()) { return response; } const to = response.headers.get("location"); if (!to) { return response; } const html = await renderRedirectPage(state.manifest, state.request, { from: state.pathname, to, status, delay: redirectDelayFor(status) }); if (html === undefined) { return response; } const headers = new Headers(response.headers); if (!headers.has("content-type")) { headers.set("content-type", "text/html"); } return new Response(html, { status, statusText: response.statusText, headers }); } /** * The composite "batteries-included" handler that wires up every request * feature internally; `astro(state)` (astro/fetch) delegates here, as does * `BaseApp.render`'s default-handler fast path. */ export async function handleRequest(state) { // Resolve the user-configured logger destination before anything logs. // Memoized single-flight — on facade-driven requests this is already a // resolved promise (BaseApp.render awaits it first). await getResolvedLogger(state.manifest); // handleRequest is the "batteries-included" handler that wires up // every pipeline feature internally. Mark them all as used so the // missing-feature warning in BaseApp never fires — the user didn't // forget to include anything. markFeatureUsed(state.manifest, ALL_FETCH_FEATURES); const invalidEncodingResponse = rejectInvalidEncoding(state); if (invalidEncodingResponse) { return invalidEncodingResponse; } const trailingSlashRedirect = handleTrailingSlash(state); if (trailingSlashRedirect) { return trailingSlashRedirect; } const notFoundResponse = handleNotFound(state); if (notFoundResponse) { return notFoundResponse; } return render(state); } /** * Renders a response for the given `FetchState`. Assumes trailing-slash * redirects and routeData resolution have already run. * * User-triggered rewrites (`Astro.rewrite` / `ctx.rewrite`) go through * `executeRewrite` on the current `FetchState` — they mutate the existing * state in place and re-run middleware + page dispatch. */ async function render(state) { const routeData = state.routeData; const pathname = state.pathname; const request = state.request; const { addCookieHeader } = state.renderOptions; state.status = getDefaultStatusCode(state.manifest, routeData, pathname); let response; let finalizeError; try { // `provideCache` always runs so `Astro.cache` is defined even // when caching is disabled — it registers a no-op shim that // warns once on use. `provideSession` is gated because there // is no equivalent disabled-shim contract for sessions. const sessionP = state.manifest.sessionConfig ? provideSession(state) : undefined; const cacheP = provideCache(state); if (sessionP || cacheP) await Promise.all([sessionP, cacheP]); // Track feature usage even when skipped. markFeatureUsed(state.manifest, FetchFeatures.sessions); // Redirect routes short-circuit the pipeline: no middleware, no // page dispatch, no i18n post-processing. Inline routeData.type // check to avoid a per-request function call + object overhead. if (routeData.type === "redirect") { const redirectResponse = await withRedirectPageBody(state, await renderRedirect(state)); logRequestFromState(state, { pathname, method: request.method, statusCode: redirectResponse.status, isRewrite: false, timeStart: state.timeStart }); prepareResponse(redirectResponse, { addCookieHeader }); state.logger.flush(); return redirectResponse; } // `null` when i18n is unset or the strategy is `manual` — for the // manual strategy users wire `astro:i18n.middleware(...)` into their // own `onRequest`. const i18n = getI18n(state.manifest); // When no cache provider is configured (the common case), run // the middleware + i18n pipeline directly without going through // the cache handler. This avoids a closure allocation and an // extra function call per request. if (!state.manifest.cacheProvider) { markFeatureUsed(state.manifest, FetchFeatures.cache); response = await handleMiddleware(state, actionsAndPages); if (i18n) { response = await finalizeI18n(i18n, state, response); } } else { const runPipeline = async () => { let res = await handleMiddleware(state, actionsAndPages); if (i18n) { res = await finalizeI18n(i18n, state, res); } return res; }; response = await handleCache(state, runPipeline); } logRequestFromState(state, { pathname, method: request.method, statusCode: response.status, isRewrite: state.isRewriting, timeStart: state.timeStart }); } catch (err) { state.logger.error(null, err.stack || err.message || String(err)); return renderErrorFromState(state, request, { ...state.renderOptions, status: 500, error: err, pathname: state.pathname }); } finally { // finalizeAll runs after the response is produced, so a rejection // here would otherwise escape the handler. Capture it and turn it // into a 500 below so the request always completes. try { const finalize = state.finalizeAll(); if (finalize) await finalize; } catch (err) { finalizeError = err; state.logger.error(null, err.stack || err.message || String(err)); } } if (finalizeError) { return renderErrorFromState(state, request, { ...state.renderOptions, status: 500, error: finalizeError, pathname: state.pathname }); } if (REROUTABLE_STATUS_CODES.includes(response.status) && response.body === null && !state.skipErrorReroute) { return renderErrorFromState(state, request, { ...state.renderOptions, response, status: response.status, // We don't have an error to report here. Passing null means we pass nothing intentionally // while undefined means there's no error error: response.status === 500 ? null : undefined, pathname: state.pathname }); } response = await withRedirectPageBody(state, response); prepareResponse(response, { addCookieHeader }); state.logger.flush(); return response; }