import { removeTrailingForwardSlash } from "@astrojs/internal-helpers/path"; import { BaseApp } from "../core/app/entrypoints/index.js"; import { getFirstForwardedValue, validateForwardedHeaders } from "../core/app/validate-headers.js"; import { shouldAppendForwardSlash } from "../core/build/util.js"; import { clientLocalsSymbol } from "../core/constants.js"; import { MiddlewareNoDataOrNextCalled, MiddlewareNotAResponse } from "../core/errors/errors-data.js"; import { createSafeError, isAstroError } from "../core/errors/index.js"; import { createRequest } from "../core/request.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"; import { RunnablePipeline } from "./pipeline.js"; import { getCustom404Route, getCustom500Route } from "../core/routing/helpers.js"; import { ensure404Route } from "../core/routing/astro-designed-error-pages.js"; import { matchRoute } from "../core/routing/dev.js"; import { req } from "../core/messages/runtime.js"; class AstroServerApp extends BaseApp { settings; logger; loader; manifestData; currentRenderContext = void 0; constructor(manifest, streaming = true, logger, manifestData, loader, settings, getDebugInfo) { super(manifest, streaming, settings, logger, loader, manifestData, getDebugInfo); this.settings = settings; this.logger = logger; this.loader = loader; this.manifestData = manifestData; } isDev() { return true; } /** * Updates the routes list when files change during development. * Called via HMR when new pages are added/removed. */ updateRoutes(newRoutesList) { this.manifestData = newRoutesList; this.pipeline.setManifestData(newRoutesList); ensure404Route(this.manifestData); } /** * Clears the route cache so that getStaticPaths() is re-evaluated. * Called via HMR when content collection data changes. */ clearRouteCache() { this.pipeline.clearRouteCache(); } /** * Clears the cached middleware so it is re-resolved on the next request. * Called via HMR when middleware files change. */ clearMiddleware() { this.pipeline.clearMiddleware(); } async devMatch(pathname) { const matchedRoute = await matchRoute( pathname, this.manifestData, this.pipeline, this.manifest ); if (!matchedRoute) { return void 0; } return { routeData: matchedRoute.route, resolvedPathname: matchedRoute.resolvedPathname }; } static async create(manifest, routesList, logger, loader, settings, getDebugInfo) { return new AstroServerApp(manifest, true, logger, routesList, loader, settings, getDebugInfo); } createPipeline(_streaming, manifest, settings, logger, loader, manifestData, getDebugInfo) { const pipeline = RunnablePipeline.create(manifestData, { loader, logger, manifest, settings, getDebugInfo }); return pipeline; } async createRenderContext(payload) { this.currentRenderContext = await super.createRenderContext(payload); return this.currentRenderContext; } async handleRequest({ controller, incomingRequest, incomingResponse, isHttps }) { const validated = validateForwardedHeaders( getFirstForwardedValue(incomingRequest.headers["x-forwarded-proto"]), getFirstForwardedValue(incomingRequest.headers["x-forwarded-host"]), getFirstForwardedValue(incomingRequest.headers["x-forwarded-port"]), this.manifest.allowedDomains ); const protocol = validated.protocol ?? (isHttps ? "https" : "http"); const host = validated.host ?? incomingRequest.headers[":authority"] ?? incomingRequest.headers.host; const origin = `${protocol}://${host}`; const url = new URL(origin + incomingRequest.url); let pathname; if (this.manifest.trailingSlash === "never" && !incomingRequest.url) { pathname = ""; } else { pathname = decodeURI(url.pathname); } if (this.manifest.trailingSlash === "never" && pathname === "/" && this.manifest.base !== "/") { pathname = ""; } url.pathname = removeTrailingForwardSlash(this.manifest.base) + url.pathname; if (url.pathname.endsWith("/") && !shouldAppendForwardSlash(this.manifest.trailingSlash, this.manifest.buildFormat)) { url.pathname = url.pathname.slice(0, -1); } let body = void 0; if (!(incomingRequest.method === "GET" || incomingRequest.method === "HEAD")) { let bytes = []; await new Promise((resolve) => { incomingRequest.on("data", (part) => { bytes.push(part); }); incomingRequest.on("end", resolve); }); body = Buffer.concat(bytes); } const self = this; await runWithErrorHandling({ controller, pathname, async run() { const matchedRoute = await self.devMatch(pathname); if (!matchedRoute) { throw new Error("No route matched, and default 404 route was not found."); } const request = createRequest({ url, headers: incomingRequest.headers, method: incomingRequest.method, body, logger: self.logger, isPrerendered: matchedRoute.routeData.prerender, routePattern: matchedRoute.routeData.component }); const locals = Reflect.get(incomingRequest, clientLocalsSymbol); for (const [name, value] of Object.entries(self.settings.config.server.headers ?? {})) { if (value) incomingResponse.setHeader(name, value); } const clientAddress = incomingRequest.socket.remoteAddress; const response = await self.render(request, { locals, routeData: matchedRoute.routeData, clientAddress }); await writeSSRResult(request, response, incomingResponse); }, onError(_err) { const error = createSafeError(_err); if (self.loader) { const { errorWithMetadata } = recordServerError( self.loader, self.manifest, self.logger, error ); handle500Response(self.loader, incomingResponse, errorWithMetadata); } return error; } }); } match(request, _allowPrerenderedRoutes) { return super.match(request, true); } async renderError(request, { skipMiddleware = false, error, status, response: _response, ...resolvedRenderOptions }) { if (isAstroError(error) && [MiddlewareNoDataOrNextCalled.name, MiddlewareNotAResponse.name].includes(error.name)) { throw error; } const renderRoute = async (routeData) => { try { const preloadedComponent = await this.pipeline.getComponentByRoute(routeData); const renderContext = await this.createRenderContext({ locals: resolvedRenderOptions.locals, pipeline: this.pipeline, pathname: this.getPathnameFromRequest(request), skipMiddleware, request, routeData, clientAddress: resolvedRenderOptions.clientAddress, status, shouldInjectCspMetaTags: !!this.manifest.csp }); renderContext.props.error = error; const response = await renderContext.render(preloadedComponent); if (error) { this.logger.error("router", error.stack || error.message); } return response; } catch (_err) { if (skipMiddleware === false) { return this.renderError(request, { ...resolvedRenderOptions, status: 500, skipMiddleware: true, error: _err }); } throw _err; } }; if (status === 404) { const custom404 = getCustom404Route(this.manifestData); if (custom404) { return renderRoute(custom404); } } const custom500 = getCustom500Route(this.manifestData); if (!custom500) { throw error; } else { return renderRoute(custom500); } } logRequest({ pathname, method, statusCode, isRewrite, reqTime }) { if (pathname === "/favicon.ico") { return; } this.logger.info( null, req({ url: pathname, method, statusCode, isRewrite, reqTime }) ); } } export { AstroServerApp };