authorgravatar for 92423096+nateEc@users.noreply.github.comYukun Shan <92423096+nateEc@users.noreply.github.com> 2026-09-01 07:38:19+08:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-31 16:38:19-07:00
log41adccc83e819c286dcdf32cd8b5f55af8bb0b49
tree5ef26ef582a9d15682db0b335b415dd5b2a3bd06
parentf8e4accf27415eb9f7f4106087d2a1b0186d66b9
signature Signed by PGP key B5690EEEBB952194

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


2 files changed, 44 insertions(+), 1 deletions(-)

apps/server/src/server.test.ts+37-1
......@@ -103,7 +103,7 @@ const collectQueueUntil = Effect.fn("TransferBudget.collectQueueUntil")(function
103103
104104import * as BackgroundPolicy from "./background/BackgroundPolicy.ts";
105105import * as ServerConfig from "./config.ts";
106import { makeRoutesLayer } from "./server.ts";
106import { HTTP_ROUTER_CONFIG, makeRoutesLayer } from "./server.ts";
107107import {
108108 isThreadDetailEvent,
109109 resolveAvailableEditorsForConfig,
......@@ -629,6 +629,7 @@ const buildAppUnderTest = (options?: {
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) => {
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
......@@ -123,6 +123,12 @@ import * as RelayClient from "@t3tools/shared/relayClient";
123123import { disableTailscaleServe, ensureTailscaleServe } from "@t3tools/tailscale";
124124import { 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.
128export 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(
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,