From c97417d071491d95870ff927324992bf62b0cd24 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 5 Feb 2026 23:15:34 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9A=A1=20Bolt:=20thread-safe=20metrics?= =?UTF-8?q?=20and=20deduplicated=20index=20building?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit implements several performance optimizations: 1. Deduplicated file list index construction using in-flight request joining. 2. Improved concurrency safety for content metrics using AsyncLocalStorage. 3. Reduced per-request allocations by moving TextEncoder, regexes, and extension sets to module-level constants. 4. Optimized file type detection logic for faster request processing. Co-authored-by: kojiwakayama <940749+kojiwakayama@users.noreply.github.com> --- .../adapters/fs/veryfront/read-operations.ts | 163 +++++++++++------- src/routing/api/handler.ts | 4 +- src/server/universal-handler/index.ts | 32 ++-- .../universal-handler/request-lifecycle.ts | 6 +- 4 files changed, 118 insertions(+), 87 deletions(-) diff --git a/src/platform/adapters/fs/veryfront/read-operations.ts b/src/platform/adapters/fs/veryfront/read-operations.ts index 93787674a3..9506a2feee 100644 --- a/src/platform/adapters/fs/veryfront/read-operations.ts +++ b/src/platform/adapters/fs/veryfront/read-operations.ts @@ -1,3 +1,4 @@ +import { AsyncLocalStorage } from "node:async_hooks"; import { logger } from "#veryfront/utils"; import { isFrameworkSourcePath } from "#veryfront/utils/path-utils.ts"; import { withSpan } from "#veryfront/observability/tracing/otlp-setup.ts"; @@ -12,6 +13,8 @@ import { getRequestScopedFile, setRequestScopedFile } from "./multi-project-adap import { PathNormalizer } from "./path-normalizer.ts"; import type { ResolvedContentContext } from "./types.ts"; +const TEXT_ENCODER = new TextEncoder(); + export interface ContentContextProvider { isProductionMode: () => boolean; getReleaseId: () => string | null; @@ -34,6 +37,7 @@ export interface ContentContextProvider { } const EXTENSION_PRIORITY = [".tsx", ".ts", ".jsx", ".js", ".mdx", ".md"] as const; +const EXTENSION_PRIORITY_SET = new Set(EXTENSION_PRIORITY); const IN_FLIGHT_REQUEST_TIMEOUT_MS = 15_000; const MAX_IN_FLIGHT_REQUESTS = 100; @@ -84,7 +88,7 @@ const cumulativeMetrics: CumulativeMetrics = { }; // Per-request metrics (reset each request via startRequestMetrics) -let currentRequest: PerRequestMetrics | null = null; +const requestMetricsStore = new AsyncLocalStorage(); function createFreshRequestMetrics(): PerRequestMetrics { return { @@ -101,28 +105,55 @@ function createFreshRequestMetrics(): PerRequestMetrics { }; } +const DATA_EXTENSIONS = new Set([".json", ".yaml", ".yml"]); + function detectFileType(path: string): FileType { - if (path.startsWith("pages/api/") || path.startsWith("app/api/")) return "api"; + // Check extensions first for common data types (fastest) + const lastDotIndex = path.lastIndexOf("."); + if (lastDotIndex !== -1 && DATA_EXTENSIONS.has(path.slice(lastDotIndex))) { + return "data"; + } + + // Check common directory patterns + if (path.startsWith("pages/")) { + if (path.startsWith("pages/api/")) return "api"; + return "page"; + } + + if (path.startsWith("app/")) { + if (path.startsWith("app/api/")) return "api"; + return "page"; + } + + if (path.startsWith("components/") || path.includes("/components/")) { + return "component"; + } + + // Check more expensive inclusions if (path.includes("/layout.") || path.includes("/layout/")) return "layout"; - if (path.startsWith("pages/") || path.startsWith("app/")) return "page"; - if (path.startsWith("components/") || path.includes("/components/")) return "component"; - if (path.endsWith(".json") || path.endsWith(".yaml") || path.endsWith(".yml")) return "data"; if (path.includes("config") || path.includes(".config.")) return "config"; + return "other"; } -/** Call at start of HTTP request to begin per-request tracking */ -export function startRequestMetrics(): void { - currentRequest = createFreshRequestMetrics(); +/** + * Call at start of HTTP request to begin per-request tracking. + * Returns a function to call when the request is complete. + */ +export function startRequestMetrics(callback?: () => Promise | T): Promise | T | void { + const metrics = createFreshRequestMetrics(); + if (callback) { + return requestMetricsStore.run(metrics, callback); + } } /** Call at end of HTTP request to log summary and update cumulative metrics */ export function endRequestMetrics( requestContext?: { requestId?: string; pathname?: string; mode?: string }, ): void { - if (!currentRequest) return; + const req = requestMetricsStore.getStore(); + if (!req) return; - const req = currentRequest; const durationMs = Math.round(performance.now() - req.startTime); // Compute derived metrics @@ -167,7 +198,6 @@ export function endRequestMetrics( isPreviewMode: req.isPreviewMode, }); - currentRequest = null; } type ContentMetricEvent = @@ -190,39 +220,40 @@ function logContentMetric( }, ): void { const path = details.path ?? ""; + const req = requestMetricsStore.getStore(); // Track in per-request metrics if active - if (currentRequest) { - currentRequest.filesAccessed.add(path); + if (req) { + req.filesAccessed.add(path); if (details.isPreviewMode !== undefined) { - currentRequest.isPreviewMode = details.isPreviewMode; + req.isPreviewMode = details.isPreviewMode; } switch (event) { case "REQUEST_SCOPED_HIT": - currentRequest.requestScopedHits++; + req.requestScopedHits++; recordContentCacheHit("request"); break; case "PERSISTENT_CACHE_HIT": - currentRequest.persistentCacheHits++; + req.persistentCacheHits++; recordContentCacheHit("persistent"); break; case "FILE_LIST_HIT": - currentRequest.fileListHits++; + req.fileListHits++; recordContentCacheHit("filelist"); break; case "NETWORK_FETCH": - currentRequest.networkFetches++; - currentRequest.fetchesByType[detectFileType(path)]++; + req.networkFetches++; + req.fetchesByType[detectFileType(path)]++; break; case "NETWORK_FETCH_COMPLETE": if (details.durationMs) { - currentRequest.networkMs += details.durationMs; + req.networkMs += details.durationMs; } break; case "CACHE_MISS": if (details.missReason) { - currentRequest.missReasons[details.missReason]++; + req.missReasons[details.missReason]++; } break; } @@ -277,6 +308,7 @@ export class ReadOperations { private fileListIndex: Map | null = null; private fileListIndexKey: string | null = null; + private fileListIndexPromise: Promise | null> | null = null; private fileListReadyPromise: Promise | null = null; @@ -296,11 +328,10 @@ export class ReadOperations { } clearFileListIndex(): void { - if (!this.fileListIndex) return; - - const size = this.fileListIndex.size; + const size = this.fileListIndex?.size ?? 0; this.fileListIndex = null; this.fileListIndexKey = null; + this.fileListIndexPromise = null; logger.debug("[ReadOperations] Cleared file list index", { entriesCleared: size }); } @@ -339,53 +370,48 @@ export class ReadOperations { } private async getOrBuildFileListIndex(): Promise | null> { - if (!this.getFileListCache) { - logger.debug("[ReadOperations] getOrBuildFileListIndex: no getFileListCache function"); - return null; - } + if (this.fileListIndexPromise) return this.fileListIndexPromise; - const fileList = await this.getFileListCache(); - if (!fileList) { - logger.debug( - "[ReadOperations] getOrBuildFileListIndex: getFileListCache returned null/undefined", - ); - return null; - } + this.fileListIndexPromise = (async () => { + if (!this.getFileListCache) { + logger.debug("[ReadOperations] getOrBuildFileListIndex: no getFileListCache function"); + return null; + } - const cacheCheckSample = fileList.find((f) => /welcome/i.test(f.path)); - logger.debug("[ReadOperations] getOrBuildFileListIndex: got file list from cache", { - fileListSize: fileList.length, - filesWithContent: fileList.filter((f) => f.content).length, - sampleFilePath: cacheCheckSample?.path, - sampleContentLength: cacheCheckSample?.content?.length, - sampleContentPreview: cacheCheckSample?.content?.slice(0, 200)?.replace(/\n/g, "\\n"), - }); + const fileList = await this.getFileListCache(); + if (!fileList) { + logger.debug( + "[ReadOperations] getOrBuildFileListIndex: getFileListCache returned null/undefined", + ); + return null; + } - const indexKey = `${fileList.length}:${fileList[0]?.path ?? ""}:${ - fileList[fileList.length - 1]?.path ?? "" - }`; - if (this.fileListIndex && this.fileListIndexKey === indexKey) return this.fileListIndex; + const indexKey = `${fileList.length}:${fileList[0]?.path ?? ""}:${ + fileList[fileList.length - 1]?.path ?? "" + }`; + if (this.fileListIndex && this.fileListIndexKey === indexKey) return this.fileListIndex; - const index = new Map(); - for (const file of fileList) { - if (file.content) index.set(file.path, file.content); - } + const index = new Map(); + for (const file of fileList) { + if (file.content) index.set(file.path, file.content); + } - this.fileListIndex = index; - this.fileListIndexKey = indexKey; - - const sampleFile = fileList.find((f) => /welcome/i.test(f.path)); - const sampleContent = sampleFile?.content; - logger.debug("[ReadOperations] Built file list index", { - fileListSize: fileList.length, - indexedWithContent: index.size, - sampleFilePath: sampleFile?.path, - sampleContentLength: sampleContent?.length, - sampleContentHash: sampleContent ? hashPreview(sampleContent) : undefined, - sampleContentPreview: sampleContent?.slice(0, 200)?.replace(/\n/g, "\\n"), - }); + this.fileListIndex = index; + this.fileListIndexKey = indexKey; - return index; + logger.debug("[ReadOperations] Built file list index", { + fileListSize: fileList.length, + indexedWithContent: index.size, + }); + + return index; + })(); + + try { + return await this.fileListIndexPromise; + } finally { + this.fileListIndexPromise = null; + } } private async getContentFromFileList(normalizedPath: string): Promise { @@ -428,7 +454,7 @@ export class ReadOperations { async () => { const normalizedPath = this.normalizer.normalize(path); const content = await this.fetchContent(normalizedPath); - return new TextEncoder().encode(content); + return TEXT_ENCODER.encode(content); }, { "fs.path": path }, ); @@ -461,7 +487,10 @@ export class ReadOperations { const cacheKeyPrefix = buildFileCacheKeyPrefix(ctx); const cacheKey = `${cacheKeyPrefix}:${normalizedPath}`; const isProduction = this.contextProvider?.isProductionMode() ?? false; - const hasKnownExt = EXTENSION_PRIORITY.some((ext) => apiPath.endsWith(ext)); + + // Use Set for slightly faster extension check + const lastDotIndex = apiPath.lastIndexOf("."); + const hasKnownExt = lastDotIndex !== -1 && EXTENSION_PRIORITY_SET.has(apiPath.slice(lastDotIndex)); logger.debug("[ReadOperations] fetchContent context", { path: normalizedPath, diff --git a/src/routing/api/handler.ts b/src/routing/api/handler.ts index 14da6e8bb3..aeefee937e 100644 --- a/src/routing/api/handler.ts +++ b/src/routing/api/handler.ts @@ -17,6 +17,8 @@ import { withSpan } from "#veryfront/observability/tracing/otlp-setup.ts"; export type { APIContext, APIRoute }; +const IS_APP_ROUTE_REGEX = /\/route\.(ts|js|tsx|jsx)$/; + /** * Injection interface for testing APIRouteHandler dependencies */ @@ -182,7 +184,7 @@ export class APIRouteHandler { // App Router routes are always named route.ts/js/tsx/jsx // Pages Router routes have descriptive names like articles.ts // Note: Cannot use path-based detection (/app/) as projectDir may be '/app' in production - const isAppRoute = /\/route\.(ts|js|tsx|jsx)$/.test(match.route.page); + const isAppRoute = IS_APP_ROUTE_REGEX.test(match.route.page); const response = isAppRoute ? await executeAppRoute(handler, request, match, pathname, adapter) diff --git a/src/server/universal-handler/index.ts b/src/server/universal-handler/index.ts index ec34be5f56..f20eb6d547 100644 --- a/src/server/universal-handler/index.ts +++ b/src/server/universal-handler/index.ts @@ -67,7 +67,7 @@ import { endContentMetrics, endRequestLifecycle, incrementRequestMetrics, - startContentMetrics, + runWithContentMetrics, startRequestLifecycle, startRequestTracking, timeAsync, @@ -262,8 +262,6 @@ export function createVeryfrontHandler( headers.releaseId, ); - startContentMetrics(); - // Check isolation const isolationCheck = checkRequestIsolation( headers.projectSlug, @@ -279,7 +277,8 @@ export function createVeryfrontHandler( startIsolatedRequest(headers.projectSlug, lifecycle.shouldCheckIsolation); - try { + return runWithContentMetrics(async () => { + try { await readyPromise; await timeAsync("security:load", async () => { @@ -449,20 +448,21 @@ export function createVeryfrontHandler( endRequestTracing(spanInfo.span, response.status, error); - endContentMetrics({ - requestId: lifecycle.requestId, - pathname: url.pathname, - mode: headers.environment || "unknown", - }); + endContentMetrics({ + requestId: lifecycle.requestId, + pathname: url.pathname, + mode: headers.environment || "unknown", + }); - const isTimeout = response.status === HTTP_GATEWAY_TIMEOUT; - completeRequestTracking(lifecycle.requestId, response.status, isTimeout); - completeIsolatedRequest(headers.projectSlug, lifecycle.shouldCheckIsolation, isTimeout); + const isTimeout = response.status === HTTP_GATEWAY_TIMEOUT; + completeRequestTracking(lifecycle.requestId, response.status, isTimeout); + completeIsolatedRequest(headers.projectSlug, lifecycle.shouldCheckIsolation, isTimeout); - return response; - } finally { - endRequestLifecycle(lifecycle); - } + return response; + } finally { + endRequestLifecycle(lifecycle); + } + }); }); }; diff --git a/src/server/universal-handler/request-lifecycle.ts b/src/server/universal-handler/request-lifecycle.ts index 4e7e875949..bd85df414b 100644 --- a/src/server/universal-handler/request-lifecycle.ts +++ b/src/server/universal-handler/request-lifecycle.ts @@ -82,10 +82,10 @@ export function startRequestTracking( } /** - * Start per-request content metrics tracking. + * Run a function within a per-request content metrics context. */ -export function startContentMetrics(): void { - startRequestMetrics(); +export function runWithContentMetrics(fn: () => Promise): Promise { + return startRequestMetrics(fn) as unknown as Promise; } /** From 828ad770379649df062f955d6e519c2b8b6f2250 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 5 Feb 2026 23:21:29 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9A=A1=20Bolt:=20thread-safe=20metrics?= =?UTF-8?q?=20and=20deduplicated=20index=20building?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit implements several performance optimizations: 1. Deduplicated file list index construction using in-flight request joining. 2. Improved concurrency safety for content metrics using AsyncLocalStorage. 3. Reduced per-request allocations by moving TextEncoder, regexes, and extension sets to module-level constants. 4. Optimized file type detection logic for faster request processing. 5. Fixed formatting issues identified by CI. Co-authored-by: kojiwakayama <940749+kojiwakayama@users.noreply.github.com> --- .../adapters/fs/veryfront/read-operations.ts | 4 +- src/server/universal-handler/index.ts | 298 +++++++++--------- 2 files changed, 152 insertions(+), 150 deletions(-) diff --git a/src/platform/adapters/fs/veryfront/read-operations.ts b/src/platform/adapters/fs/veryfront/read-operations.ts index 9506a2feee..525b5d3ac6 100644 --- a/src/platform/adapters/fs/veryfront/read-operations.ts +++ b/src/platform/adapters/fs/veryfront/read-operations.ts @@ -197,7 +197,6 @@ export function endRequestMetrics( uniqueFiles: req.filesAccessed.size, isPreviewMode: req.isPreviewMode, }); - } type ContentMetricEvent = @@ -490,7 +489,8 @@ export class ReadOperations { // Use Set for slightly faster extension check const lastDotIndex = apiPath.lastIndexOf("."); - const hasKnownExt = lastDotIndex !== -1 && EXTENSION_PRIORITY_SET.has(apiPath.slice(lastDotIndex)); + const hasKnownExt = lastDotIndex !== -1 && + EXTENSION_PRIORITY_SET.has(apiPath.slice(lastDotIndex)); logger.debug("[ReadOperations] fetchContent context", { path: normalizedPath, diff --git a/src/server/universal-handler/index.ts b/src/server/universal-handler/index.ts index f20eb6d547..5f45e741d8 100644 --- a/src/server/universal-handler/index.ts +++ b/src/server/universal-handler/index.ts @@ -279,174 +279,176 @@ export function createVeryfrontHandler( return runWithContentMetrics(async () => { try { - await readyPromise; - - await timeAsync("security:load", async () => { - if (isProxyMode) return; - await securityLoader.ensureLoaded(); - }); - - await timeAsync("config:load", async () => { - await configPromise; - }); - - const executeHandler = async (): Promise => { - const reqCtx = createRequestContext(req, opts.envConfig); - - const wsSlugOverride = url.searchParams.get("x-project-slug") || undefined; - - // Resolve project from various sources - const projectRes = await resolveProject(req, url, headers, { - config, - reqCtx, - defaultProjectSlug: opts.defaultProjectSlug, - defaultProjectId: opts.defaultProjectId, - wsSlugOverride, - }); - - setProjectAttributes(spanInfo.span, projectRes.projectSlug, projectRes.proxyEnv); - - // Handle projects discovery UI - if ( - shouldHandleProjectsUI(url.pathname, projectRes.projectSlug, projectRes.parsedDomain) - ) { - const response = await handleProjectsRequest( - req, - url, - buildMinimalContext( - projectDir, - adapter, - securityLoader.getSecurityConfig(), - securityLoader.getCspUserHeader(), - opts.debug, - config, - ), - ); - if (response) return response; - } - - // Resolve adapter and config for project - const adapterRes = await resolveAdapter({ - projectDir, - adapter, - config, - projectSlug: projectRes.projectSlug, - projectId: projectRes.projectId, - proxyToken: reqCtx.token, - releaseId: projectRes.releaseId, - proxyEnv: projectRes.proxyEnv, - branch: reqCtx.branch, - environmentName: projectRes.environmentName, - parsedDomain: projectRes.parsedDomain, - headerProjectPath: headers.projectPath, - isProxyMode, - }); + await readyPromise; - // Resolve environment and validate - const host = req.headers.get("x-forwarded-host") || req.headers.get("host") || url.host; - const envRes = resolveEnvironment({ - proxyEnv: projectRes.proxyEnv, - reqCtxMode: reqCtx.mode, - releaseId: projectRes.releaseId, - projectSlug: projectRes.projectSlug, - projectId: projectRes.projectId, - environmentName: projectRes.environmentName, - host, - isLocalProject: adapterRes.isLocalProject, - isProxyMode, - isLocalDev: reqCtx.isLocalDev, - pathname: url.pathname, - defaultEnvironment: opts.defaultEnvironment, + await timeAsync("security:load", async () => { + if (isProxyMode) return; + await securityLoader.ensureLoaded(); }); - if (envRes.errorResponse) { - return envRes.errorResponse; - } - - // Build handler context - const ctx = buildHandlerContext({ - projectDir: adapterRes.projectDir, - adapter: adapterRes.adapter, - securityConfig: securityLoader.getSecurityConfig(), - cspUserHeader: securityLoader.getCspUserHeader(), - debug: opts.debug, - config: adapterRes.config, - parsedDomain: projectRes.parsedDomain, - projectSlug: projectRes.projectSlug, - projectId: projectRes.projectId, - releaseId: envRes.releaseId, - proxyToken: reqCtx.token, - environmentName: projectRes.environmentName, - resolvedEnvironment: envRes.resolvedEnvironment ?? "preview", - requestContext: reqCtx, - routeRegistry: registry, - isLocalProject: adapterRes.isLocalProject, - moduleServerUrl: opts.moduleServerUrl, + await timeAsync("config:load", async () => { + await configPromise; }); - await incrementRequestMetrics(); + const executeHandler = async (): Promise => { + const reqCtx = createRequestContext(req, opts.envConfig); - const response = await withSpan( - SpanNames.HANDLER_EXECUTE, - () => registry.execute(req, ctx), - { - "handler.project_slug": projectRes.projectSlug || "unknown", - "handler.path": url.pathname, - "handler.method": req.method, - }, - ); + const wsSlugOverride = url.searchParams.get("x-project-slug") || undefined; - if (response) return response; + // Resolve project from various sources + const projectRes = await resolveProject(req, url, headers, { + config, + reqCtx, + defaultProjectSlug: opts.defaultProjectSlug, + defaultProjectId: opts.defaultProjectId, + wsSlugOverride, + }); - logDebug("[universal] No handler produced response (unexpected)", { path: url.pathname }); - return new Response(ErrorPages.serverError(), { - status: 500, - headers: { "Content-Type": "text/html; charset=utf-8" }, - }); - }; + setProjectAttributes(spanInfo.span, projectRes.projectSlug, projectRes.proxyEnv); + + // Handle projects discovery UI + if ( + shouldHandleProjectsUI(url.pathname, projectRes.projectSlug, projectRes.parsedDomain) + ) { + const response = await handleProjectsRequest( + req, + url, + buildMinimalContext( + projectDir, + adapter, + securityLoader.getSecurityConfig(), + securityLoader.getCspUserHeader(), + opts.debug, + config, + ), + ); + if (response) return response; + } + + // Resolve adapter and config for project + const adapterRes = await resolveAdapter({ + projectDir, + adapter, + config, + projectSlug: projectRes.projectSlug, + projectId: projectRes.projectId, + proxyToken: reqCtx.token, + releaseId: projectRes.releaseId, + proxyEnv: projectRes.proxyEnv, + branch: reqCtx.branch, + environmentName: projectRes.environmentName, + parsedDomain: projectRes.parsedDomain, + headerProjectPath: headers.projectPath, + isProxyMode, + }); - let response: Response; - let error: Error | undefined; - let timeoutId: ReturnType | undefined; + // Resolve environment and validate + const host = req.headers.get("x-forwarded-host") || req.headers.get("host") || url.host; + const envRes = resolveEnvironment({ + proxyEnv: projectRes.proxyEnv, + reqCtxMode: reqCtx.mode, + releaseId: projectRes.releaseId, + projectSlug: projectRes.projectSlug, + projectId: projectRes.projectId, + environmentName: projectRes.environmentName, + host, + isLocalProject: adapterRes.isLocalProject, + isProxyMode, + isLocalDev: reqCtx.isLocalDev, + pathname: url.pathname, + defaultEnvironment: opts.defaultEnvironment, + }); - try { - response = await Promise.race([ - executeWithTracingContext(spanInfo, executeHandler), - new Promise((_, reject) => { - timeoutId = setTimeout(() => reject(TIMEOUT_SENTINEL), getRequestTimeout()); - }), - ]); - } catch (e) { - if (e === TIMEOUT_SENTINEL) { - logger.warn("[universal] Request timed out", { - path: url.pathname, - method: req.method, - timeoutMs: getRequestTimeout(), + if (envRes.errorResponse) { + return envRes.errorResponse; + } + + // Build handler context + const ctx = buildHandlerContext({ + projectDir: adapterRes.projectDir, + adapter: adapterRes.adapter, + securityConfig: securityLoader.getSecurityConfig(), + cspUserHeader: securityLoader.getCspUserHeader(), + debug: opts.debug, + config: adapterRes.config, + parsedDomain: projectRes.parsedDomain, + projectSlug: projectRes.projectSlug, + projectId: projectRes.projectId, + releaseId: envRes.releaseId, + proxyToken: reqCtx.token, + environmentName: projectRes.environmentName, + resolvedEnvironment: envRes.resolvedEnvironment ?? "preview", + requestContext: reqCtx, + routeRegistry: registry, + isLocalProject: adapterRes.isLocalProject, + moduleServerUrl: opts.moduleServerUrl, }); - response = new Response( - JSON.stringify({ - error: "Request timeout", - timeoutMs: getRequestTimeout(), - path: url.pathname, - }), + await incrementRequestMetrics(); + + const response = await withSpan( + SpanNames.HANDLER_EXECUTE, + () => registry.execute(req, ctx), { - status: HTTP_GATEWAY_TIMEOUT, - headers: { "Content-Type": "application/json" }, + "handler.project_slug": projectRes.projectSlug || "unknown", + "handler.path": url.pathname, + "handler.method": req.method, }, ); - } else { - error = e instanceof Error ? e : new Error(String(e)); - response = new Response(ErrorPages.serverError(), { + + if (response) return response; + + logDebug("[universal] No handler produced response (unexpected)", { + path: url.pathname, + }); + return new Response(ErrorPages.serverError(), { status: 500, headers: { "Content-Type": "text/html; charset=utf-8" }, }); + }; + + let response: Response; + let error: Error | undefined; + let timeoutId: ReturnType | undefined; + + try { + response = await Promise.race([ + executeWithTracingContext(spanInfo, executeHandler), + new Promise((_, reject) => { + timeoutId = setTimeout(() => reject(TIMEOUT_SENTINEL), getRequestTimeout()); + }), + ]); + } catch (e) { + if (e === TIMEOUT_SENTINEL) { + logger.warn("[universal] Request timed out", { + path: url.pathname, + method: req.method, + timeoutMs: getRequestTimeout(), + }); + + response = new Response( + JSON.stringify({ + error: "Request timeout", + timeoutMs: getRequestTimeout(), + path: url.pathname, + }), + { + status: HTTP_GATEWAY_TIMEOUT, + headers: { "Content-Type": "application/json" }, + }, + ); + } else { + error = e instanceof Error ? e : new Error(String(e)); + response = new Response(ErrorPages.serverError(), { + status: 500, + headers: { "Content-Type": "text/html; charset=utf-8" }, + }); + } + } finally { + if (timeoutId !== undefined) clearTimeout(timeoutId); } - } finally { - if (timeoutId !== undefined) clearTimeout(timeoutId); - } - endRequestTracing(spanInfo.span, response.status, error); + endRequestTracing(spanInfo.span, response.status, error); endContentMetrics({ requestId: lifecycle.requestId,