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
JavaScript
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;
}