diff --git a/src/server/runtime-handler/index.ts b/src/server/runtime-handler/index.ts index 9cd6610293..21474e9b6e 100644 --- a/src/server/runtime-handler/index.ts +++ b/src/server/runtime-handler/index.ts @@ -86,7 +86,12 @@ import { localProjectCache } from "./local-project-discovery.ts"; import { resolveEnvironment } from "./environment-resolution.ts"; import { buildHandlerContext, buildMinimalContext } from "./handler-context-builder.ts"; import { handleProjectsRequest, shouldHandleProjectsUI } from "./projects-handler.ts"; -import { HTTP_GATEWAY_TIMEOUT, isLightweightPath, isMonitoringPath } from "./request-utils.ts"; +import { + HTTP_GATEWAY_TIMEOUT, + isLightweightPath, + isMonitoringPath, + isWebSocketPath, +} from "./request-utils.ts"; import { withRequestTimeout } from "./timeout-manager.ts"; import { EnvironmentVariableCache, @@ -309,7 +314,7 @@ export function createVeryfrontHandler( // Early validation: in proxy mode, required context headers must be present. // Without these, the server cannot authenticate or resolve the project, and // proceeding would cause cryptic 500s deep in the rendering pipeline. - if (isProxyMode && !isLightweightPath(url.pathname)) { + if (isProxyMode && !isLightweightPath(url.pathname) && !isWebSocketPath(url.pathname)) { const token = req.headers.get("x-token"); if (!headers.projectSlug) { logger.error("Missing required x-project-slug header in proxy mode", { diff --git a/src/server/runtime-handler/request-tracker.ts b/src/server/runtime-handler/request-tracker.ts index ceb4b7503b..12b30a4a12 100644 --- a/src/server/runtime-handler/request-tracker.ts +++ b/src/server/runtime-handler/request-tracker.ts @@ -7,6 +7,7 @@ import { serverLogger } from "#veryfront/utils"; import { unrefTimer } from "#veryfront/compat/process.ts"; +import { isWebSocketPath } from "./request-utils.ts"; const logger = serverLogger.component("request-tracker"); @@ -90,31 +91,34 @@ class RequestTracker { releaseId, }; - tracked.slowTimer = setTimeout(() => { - const elapsedMs = Math.round(performance.now() - startTime); - logger.warn("Slow request detected", { - requestId, - projectSlug, - path, - method, - elapsedMs, - inFlightCount: this.inFlight.size, - }); - + // WebSocket connections are long-lived by design — don't flag them as stuck. + if (!isWebSocketPath(path)) { tracked.slowTimer = setTimeout(() => { - const verySlowElapsedMs = Math.round(performance.now() - startTime); - logger.error("Very slow request - likely stuck", { + const elapsedMs = Math.round(performance.now() - startTime); + logger.warn("Slow request detected", { requestId, projectSlug, path, method, - elapsedMs: verySlowElapsedMs, + elapsedMs, inFlightCount: this.inFlight.size, }); - }, VERY_SLOW_REQUEST_THRESHOLD_MS - SLOW_REQUEST_THRESHOLD_MS); + + tracked.slowTimer = setTimeout(() => { + const verySlowElapsedMs = Math.round(performance.now() - startTime); + logger.error("Very slow request - likely stuck", { + requestId, + projectSlug, + path, + method, + elapsedMs: verySlowElapsedMs, + inFlightCount: this.inFlight.size, + }); + }, VERY_SLOW_REQUEST_THRESHOLD_MS - SLOW_REQUEST_THRESHOLD_MS); + if (tracked.slowTimer) unrefTimer(tracked.slowTimer); + }, SLOW_REQUEST_THRESHOLD_MS); if (tracked.slowTimer) unrefTimer(tracked.slowTimer); - }, SLOW_REQUEST_THRESHOLD_MS); - if (tracked.slowTimer) unrefTimer(tracked.slowTimer); + } this.inFlight.set(requestId, tracked); diff --git a/src/server/runtime-handler/request-utils.test.ts b/src/server/runtime-handler/request-utils.test.ts index 66ef386e8b..eb0213378b 100644 --- a/src/server/runtime-handler/request-utils.test.ts +++ b/src/server/runtime-handler/request-utils.test.ts @@ -5,6 +5,7 @@ import { isInternalHost, isLightweightPath, isMonitoringPath, + isWebSocketPath, LIGHTWEIGHT_PATH_PREFIXES, MONITORING_PATHS, TIMEOUT_SENTINEL, @@ -112,4 +113,16 @@ describe("request-utils", () => { assertEquals(isLightweightPath("/api/users"), false); }); }); + + describe("isWebSocketPath", () => { + it("returns true for /_ws", () => { + assertEquals(isWebSocketPath("/_ws"), true); + }); + + it("returns false for other paths", () => { + assertEquals(isWebSocketPath("/"), false); + assertEquals(isWebSocketPath("/_ws/sub"), false); + assertEquals(isWebSocketPath("/_wss"), false); + }); + }); }); diff --git a/src/server/runtime-handler/request-utils.ts b/src/server/runtime-handler/request-utils.ts index a72f333ab0..7510ec1d42 100644 --- a/src/server/runtime-handler/request-utils.ts +++ b/src/server/runtime-handler/request-utils.ts @@ -69,3 +69,8 @@ export const LIGHTWEIGHT_PATH_PREFIXES = [ export function isLightweightPath(pathname: string): boolean { return LIGHTWEIGHT_PATH_PREFIXES.some((prefix) => pathname.startsWith(prefix)); } + +/** Check if path is the WebSocket endpoint (long-lived, handled by HMR handler) */ +export function isWebSocketPath(pathname: string): boolean { + return pathname === "/_ws"; +}