History

fix(server): allow long thread IDs in HTTP routes (#8898)

Yukun Shan committed 2 months ago

Commit 41adccc1 parent f8e4acc2 files · +44 −1Browse files

Unified · Split

2 files
  1. apps/server/src/server.test.ts +37 −1
  2. apps/server/src/server.ts +7 −0
apps/server/src/server.test.ts+37−1
@@ -103,7 +103,7 @@ const collectQueueUntil = Effect.fn("TransferBudget.collectQueueUntil")(function · 102 unmodified lines
103103
104104 import * as BackgroundPolicy from "./background/BackgroundPolicy.ts";
105105 import * as ServerConfig from "./config.ts";
106−import { makeRoutesLayer } from "./server.ts";
106+import { HTTP_ROUTER_CONFIG, makeRoutesLayer } from "./server.ts";
107107 import {
108108 isThreadDetailEvent,
109109 resolveAvailableEditorsForConfig,
@@ -629,6 +629,7 @@ const buildAppUnderTest = (options?: { · 519 unmodified lines
629629 {
630630 disableListenLog: true,
631631 disableLogger: true,
632+ routerConfig: HTTP_ROUTER_CONFIG,
632633 },
633634 ).pipe(
634635 Layer.provide(
@@ -1527,6 +1528,41 @@ it.layer(NodeServices.layer)("server router seam", (it) => { · 892 unmodified lines
15271528 }).pipe(Effect.provide(NodeHttpServer.layerTest)),
15281529 );
15291530
1531+ it.effect("serves snapshots for MCP handoff thread IDs above the router default", () =>
1532+ Effect.gen(function* () {
1533+ const threadId = ThreadId.make(
1534+ "thread:mcp:abfba0d2-b591-4b7e-aad1-e943d89811fa:handoff%3A0ae5edf4-2ea3-4ee3-ba7c-48de3ac92896%3A2026-08-24T17%3A08%3A52.138Z:0",
1535+ );
1536+ const thread = {
1537+ ...makeDefaultOrchestrationReadModel().threads[0]!,
1538+ id: threadId,
1539+ };
1540+ yield* buildAppUnderTest({
1541+ layers: {
1542+ projectionSnapshotQuery: {
1543+ getThreadDetailSnapshot: (requestedThreadId) =>
1544+ Effect.succeed(
1545+ requestedThreadId === threadId
1546+ ? Option.some({ snapshotSequence: 1, thread })
1547+ : Option.none(),
1548+ ),
1549+ },
1550+ },
1551+ });
1552+
1553+ const response = yield* fetchEffect(
1554+ yield* getHttpServerUrl(`/api/orchestration/threads/${encodeURIComponent(threadId)}`),
1555+ { headers: { cookie: yield* getAuthenticatedSessionCookieHeader() } },
1556+ );
1557+ const snapshot = yield* responseJsonEffect<{
1558+ readonly thread: { readonly id: ThreadId };
1559+ }>(response);
1560+
1561+ assert.equal(response.status, 200);
1562+ assert.equal(snapshot.thread.id, threadId);
1563+ }).pipe(Effect.provide(NodeHttpServer.layerTest)),
1564+ );
1565+
15301566 it.effect("compresses large JSON responses through the composed routes", () =>
15311567 Effect.gen(function* () {
15321568 const descriptor = {
apps/server/src/server.ts+7−0
@@ -123,6 +123,12 @@ import * as RelayClient from "@t3tools/shared/relayClient"; · 122 unmodified lines
123123 import { disableTailscaleServe, ensureTailscaleServe } from "@t3tools/tailscale";
124124 import { forkParked, ServerActivation } from "./serverActivation.ts";
125125
126+// MCP handoff thread IDs include escaped provenance and can exceed find-my-way's
127+// 100-character default for one path segment.
128+export const HTTP_ROUTER_CONFIG = {
129+ maxParamLength: 512,
130+} as const;
131+
126132 // Effect's default preemptive shutdown waits 20s before finalizing request scopes.
127133 // T3's primary transport is long-lived WebSocket RPC, whose Effect scope finalizer
128134 // already closes the websocket gracefully. Do not add an artificial drain before
@@ -675,6 +681,7 @@ export const makeServerLayer = Layer.unwrap( · 546 unmodified lines
675681
676682 const routesLayer = HttpRouter.serve(makeRoutesLayer.pipe(Layer.provide(launcherLayer)), {
677683 disableLogger: !config.logWebSocketEvents,
684+ routerConfig: HTTP_ROUTER_CONFIG,
678685 }).pipe(Layer.tap(() => Deferred.succeed(routesReady, undefined).pipe(Effect.orDie)));
679686 const serverApplicationLayer = Layer.mergeAll(
680687 routesLayer,