astro
Version:
Astro is a modern site builder with web best practices, performance, and DX front-of-mind.
171 lines (170 loc) • 6.31 kB
JavaScript
import { ActionHandler } from "../../actions/handler.js";
import {
REROUTABLE_STATUS_CODES,
REROUTE_DIRECTIVE_HEADER,
REWRITE_DIRECTIVE_HEADER_KEY
} from "../constants.js";
import { TrailingSlashHandler } from "./trailing-slash-handler.js";
import { CacheHandler, provideCache } from "../cache/handler.js";
import { I18n } from "../i18n/handler.js";
import { AstroMiddleware } from "../middleware/astro-middleware.js";
import { PagesHandler } from "../pages/handler.js";
import { renderRedirect } from "../redirects/render.js";
import { provideSession } from "../session/handler.js";
import { prepareResponse } from "../app/prepare-response.js";
import { PipelineFeatures } from "../base-pipeline.js";
class AstroHandler {
#app;
#trailingSlashHandler;
#actionHandler;
#astroMiddleware;
#pagesHandler;
#cacheHandler;
/** Bound callback for the middleware chain — created once, reused per request. */
#renderRouteCallback;
/**
* i18n post-processor. Only set when the app has i18n configured and
* the strategy is not `manual` — for the manual strategy users wire
* `astro:i18n.middleware(...)` into their own `onRequest`.
*/
#i18n;
/** Whether sessions are configured on the manifest. */
#hasSession;
constructor(app) {
this.#app = app;
this.#trailingSlashHandler = new TrailingSlashHandler(app);
this.#actionHandler = new ActionHandler();
this.#astroMiddleware = new AstroMiddleware(app.pipeline);
this.#pagesHandler = new PagesHandler(app.pipeline);
this.#cacheHandler = new CacheHandler(app);
this.#renderRouteCallback = this.#actionsAndPages.bind(this);
this.#hasSession = !!app.manifest.sessionConfig;
const i18n = app.manifest.i18n;
if (i18n && i18n.strategy !== "manual") {
this.#i18n = new I18n(
i18n,
app.manifest.base,
app.manifest.trailingSlash,
app.manifest.buildFormat
);
}
}
/**
* Runs actions then pages — the callback at the bottom of the
* middleware chain. Bound once in the constructor to avoid
* per-request closure allocation.
*/
#actionsAndPages(state, ctx) {
if (!state.skipMiddleware) {
const actionResult = this.#actionHandler.handle(ctx, state);
if (actionResult) {
return actionResult.then((response) => response ?? this.#pagesHandler.handle(state, ctx));
}
}
return this.#pagesHandler.handle(state, ctx);
}
async handle(state) {
const trailingSlashRedirect = this.#trailingSlashHandler.handle(state);
if (trailingSlashRedirect) {
return trailingSlashRedirect;
}
if (!state.routeData) {
return this.#app.renderError(state.request, {
...state.renderOptions,
status: 404,
pathname: state.pathname
});
}
return this.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
* `Rewrites.execute` on the current `FetchState` — they mutate the
* existing state in place and re-run middleware + page dispatch.
*/
async render(state) {
const routeData = state.routeData;
const pathname = state.pathname;
const request = state.request;
const { addCookieHeader } = state.renderOptions;
const defaultStatus = this.#app.getDefaultStatusCode(routeData, pathname);
state.status = defaultStatus;
let response;
try {
const sessionP = this.#hasSession ? provideSession(state) : void 0;
const cacheP = provideCache(state);
if (sessionP || cacheP) await Promise.all([sessionP, cacheP]);
state.pipeline.usedFeatures |= PipelineFeatures.sessions;
if (routeData.type === "redirect") {
const redirectResponse = await renderRedirect(state);
this.#app.logThisRequest({
pathname,
method: request.method,
statusCode: redirectResponse.status,
isRewrite: false,
timeStart: state.timeStart
});
prepareResponse(redirectResponse, { addCookieHeader });
this.#app.pipeline.logger.flush();
return redirectResponse;
}
if (!this.#app.pipeline.cacheProvider) {
this.#app.pipeline.usedFeatures |= PipelineFeatures.cache;
response = await this.#astroMiddleware.handle(state, this.#renderRouteCallback);
if (this.#i18n) {
response = await this.#i18n.finalize(state, response);
}
} else {
const runPipeline = async () => {
let res = await this.#astroMiddleware.handle(state, this.#renderRouteCallback);
if (this.#i18n) {
res = await this.#i18n.finalize(state, res);
}
return res;
};
response = await this.#cacheHandler.handle(state, runPipeline);
}
const isRewrite = response.headers.has(REWRITE_DIRECTIVE_HEADER_KEY);
this.#app.logThisRequest({
pathname,
method: request.method,
statusCode: response.status,
isRewrite,
timeStart: state.timeStart
});
} catch (err) {
this.#app.logger.error(null, err.stack || err.message || String(err));
return this.#app.renderError(request, {
...state.renderOptions,
status: 500,
error: err,
pathname: state.pathname
});
} finally {
const finalize = state.finalizeAll();
if (finalize) await finalize;
}
if (REROUTABLE_STATUS_CODES.includes(response.status) && // If the body isn't null, that means the user sets the 404 status
// but uses the current route to handle the 404
response.body === null && response.headers.get(REROUTE_DIRECTIVE_HEADER) !== "no") {
return this.#app.renderError(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 : void 0,
pathname: state.pathname
});
}
prepareResponse(response, { addCookieHeader });
this.#app.pipeline.logger.flush();
return response;
}
}
export {
AstroHandler
};