import log from "../log/mod"; import { Version } from "../conf/version"; import reqtrack, { Context } from "../reqtrack"; import { traceIDToString } from "../reqtrack/encoding"; import { logCtx, withLogCtx } from "../reqtrack/logging"; import { newSpanID, newTraceID, parseTraceParent, TraceParentHeader, } from "../reqtrack/tracecontext"; import { now, since, toDurationStr } from "../utils/timers"; import Router, { RouteContext } from "./router"; import { ApiRoute, Request, Response, StatusCode } from "./types"; import { writeError } from "./serde/json"; /** * Creates a router to handle routing the API requests made * to the Encore application */ export function createApiRouter(routes: ApiRoute[]): Router { const router = new Router(); const logEachRegistration = routes.length < 8; for (const route of routes) { const logCtx: logCtx = { service: route.service, endpoint: route.name, }; // Create a handler for the API route const handler = async (ctx: RouteContext, req: Request, res: Response) => { return withLogCtx(logCtx, async () => { log.info("starting request"); const start = now(); try { await route.handler(req, res, ctx.params); } catch (e: unknown) { log.error(e, "request failed"); throw e; } finally { log.info("request completed", { duration: toDurationStr(since(start)), }); } }); }; // Register the route with the router for each HTTP method it supports for (const method of route.methods) { router.register(method, route.path, handler); } if (logEachRegistration) { log.info("registered API endpoint", { service: route.service, path: route.path, endpoint: route.name, }); } } if (!logEachRegistration) { log.info(`registered ${routes.length} API endpoints`); } return router; } /** * this function is the entrypoint for all HTTP requests into the Encore app, it's job is to find the correct route * and execute the handler for that route. */ export default async function handler( apis: Router, encoreInternal: Router, req: Request, res: Response, ) { try { res.appendHeader("X-Powered-By", "Encore/JS/" + Version); // Parse the trace context from the request headers and create our tracking context // eslint-disable-next-line prefer-const let [traceID, parentSpanID] = parseTraceParent( firstHeader(req, TraceParentHeader) ?? "", ); if (traceID === undefined) { traceID = newTraceID(); } const spanID = newSpanID(); const ctx: Context = { traceID: traceID, spanID: spanID, parentSpanID: parentSpanID, extRequestID: firstHeader(req, "X-Request-ID"), correlationID: firstHeader(req, "X-Correlation-ID"), }; // Always send the trace id back. const traceIDStr = traceIDToString(traceID); res.appendHeader("X-Encore-Trace-Id", traceIDStr); // Echo the X-Request-ID back to the caller if present, // otherwise send back the trace id. const reqID = ctx.extRequestID ?? traceIDStr; res.appendHeader("X-Request-Id", reqID); // note: the await here is important, it that our catch below will catch any errors thrown by the handler // and write them to the response. return await reqtrack.run(ctx, () => { // Switch to the Encore router if the request is for an Encore internal endpoint let router = apis; if (req.url.pathname.startsWith("/__encore/")) { router = encoreInternal; } // Find the route const routeCtx = router.find(req.method, req.url.pathname); if (routeCtx === null) { return writeError(res, StatusCode.NOT_FOUND, new Error("not found")); } // Execute the route handler return routeCtx.handler(routeCtx, req, res); }); } catch (e: unknown) { return writeError(res, StatusCode.INTERNAL, e as Error); } } function firstHeader(req: Request, name: string): string | undefined { const headers = req.header(name); if (headers.length === 0) { return undefined; } return headers[0]; }