feat(config): lower MCP timeouts and raise JSON body limit
Reduce the default external-MCP silence timeout from 5 min to 1 min and the overall call timeout from 15 min to 2 min. Update related tests and comments. Increase Fastify's JSON body limit to 25 MiB (configurable via HTTP_JSON_BODY_LIMIT) to accommodate large AI‑chat payloads. BREAKING CHANGE: shorter MCP timeouts may abort long‑running tool calls that previously succeeded with the older defaults.
This commit is contained in:
@@ -181,25 +181,25 @@ describe('mcp timeout env helpers', () => {
|
||||
else process.env.AI_MCP_CALL_TIMEOUT_MS = ORIG_CALL;
|
||||
});
|
||||
|
||||
it('mcpStreamTimeoutMs defaults to 5 min and honors a positive override', () => {
|
||||
it('mcpStreamTimeoutMs defaults to 1 min and honors a positive override', () => {
|
||||
delete process.env.AI_MCP_STREAM_TIMEOUT_MS;
|
||||
expect(mcpStreamTimeoutMs()).toBe(300_000);
|
||||
process.env.AI_MCP_STREAM_TIMEOUT_MS = '60000';
|
||||
expect(mcpStreamTimeoutMs()).toBe(60_000);
|
||||
process.env.AI_MCP_STREAM_TIMEOUT_MS = '90000';
|
||||
expect(mcpStreamTimeoutMs()).toBe(90_000);
|
||||
for (const bad of ['0', '-1', 'x', '']) {
|
||||
process.env.AI_MCP_STREAM_TIMEOUT_MS = bad;
|
||||
expect(mcpStreamTimeoutMs()).toBe(300_000);
|
||||
expect(mcpStreamTimeoutMs()).toBe(60_000);
|
||||
}
|
||||
});
|
||||
|
||||
it('mcpCallTimeoutMs defaults to 15 min and honors a positive override', () => {
|
||||
it('mcpCallTimeoutMs defaults to 2 min and honors a positive override', () => {
|
||||
delete process.env.AI_MCP_CALL_TIMEOUT_MS;
|
||||
expect(mcpCallTimeoutMs()).toBe(900_000);
|
||||
process.env.AI_MCP_CALL_TIMEOUT_MS = '120000';
|
||||
expect(mcpCallTimeoutMs()).toBe(120_000);
|
||||
process.env.AI_MCP_CALL_TIMEOUT_MS = '180000';
|
||||
expect(mcpCallTimeoutMs()).toBe(180_000);
|
||||
for (const bad of ['0', '-1', 'x', '']) {
|
||||
process.env.AI_MCP_CALL_TIMEOUT_MS = bad;
|
||||
expect(mcpCallTimeoutMs()).toBe(900_000);
|
||||
expect(mcpCallTimeoutMs()).toBe(120_000);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -523,12 +523,12 @@ export function validateResolvedAddresses(addrs: readonly LookupAddress[]): {
|
||||
*/
|
||||
function buildPinnedDispatcher(): Agent {
|
||||
// External-MCP traffic uses a DEDICATED, shorter silence timeout
|
||||
// (`AI_MCP_STREAM_TIMEOUT_MS`, default 5 min) — deliberately tighter than the
|
||||
// (`AI_MCP_STREAM_TIMEOUT_MS`, default 1 min) — deliberately tighter than the
|
||||
// chat provider's 15-min `streamTimeoutMs()` — so a byte-silent/hung MCP
|
||||
// upstream is broken in ~5 min instead of 15. We keep the keep-alive options
|
||||
// upstream is broken in ~1 min instead of 15. We keep the keep-alive options
|
||||
// from `streamingDispatcherOptions()` but OVERRIDE headers/body timeouts.
|
||||
// Accepted trade-off: a legitimately long but byte-silent single tool call,
|
||||
// and an SSE transport idling >5 min BETWEEN tool calls, are also cut here; the
|
||||
// and an SSE transport idling >1 min BETWEEN tool calls, are also cut here; the
|
||||
// per-call total cap (wrapToolsWithCallTimeout, `AI_MCP_CALL_TIMEOUT_MS`) is the
|
||||
// complementary guard for chatty-but-stuck calls that keep the socket warm yet
|
||||
// never return.
|
||||
|
||||
@@ -123,19 +123,19 @@ export function streamKeepAliveMs(): number {
|
||||
return positiveEnv('AI_STREAM_KEEPALIVE_MS', DEFAULT_STREAM_KEEPALIVE_MS);
|
||||
}
|
||||
|
||||
/** Default SILENCE timeout for EXTERNAL-MCP transport (5 min). */
|
||||
const DEFAULT_MCP_STREAM_TIMEOUT_MS = 300_000;
|
||||
/** Default SILENCE timeout for EXTERNAL-MCP transport (1 min). */
|
||||
const DEFAULT_MCP_STREAM_TIMEOUT_MS = 60_000;
|
||||
|
||||
/** Default total wall-clock cap for ONE external MCP tool call (15 min). */
|
||||
const DEFAULT_MCP_CALL_TIMEOUT_MS = 900_000;
|
||||
/** Default total wall-clock cap for ONE external MCP tool call (2 min). */
|
||||
const DEFAULT_MCP_CALL_TIMEOUT_MS = 120_000;
|
||||
|
||||
/**
|
||||
* SILENCE timeout (ms) for EXTERNAL-MCP transport ONLY. Override with
|
||||
* `AI_MCP_STREAM_TIMEOUT_MS`; a missing/invalid/non-positive value falls back to
|
||||
* {@link DEFAULT_MCP_STREAM_TIMEOUT_MS} (5 min).
|
||||
* {@link DEFAULT_MCP_STREAM_TIMEOUT_MS} (1 min).
|
||||
*
|
||||
* Deliberately tighter than the chat provider's {@link streamTimeoutMs} (15 min)
|
||||
* so a byte-silent/hung MCP upstream is broken in ~5 min instead of 15. This is
|
||||
* so a byte-silent/hung MCP upstream is broken in ~1 min instead of 15. This is
|
||||
* the undici `headersTimeout`/`bodyTimeout` for the external-MCP dispatcher only
|
||||
* — it must NOT change the chat provider, which legitimately needs 15 min between
|
||||
* reasoning chunks (#175).
|
||||
@@ -153,7 +153,7 @@ export function mcpStreamTimeoutMs(): number {
|
||||
/**
|
||||
* Total wall-clock cap (ms) for ONE external MCP tool call — APP-LEVEL, not
|
||||
* transport. Override with `AI_MCP_CALL_TIMEOUT_MS`; a missing/invalid/
|
||||
* non-positive value falls back to {@link DEFAULT_MCP_CALL_TIMEOUT_MS} (15 min).
|
||||
* non-positive value falls back to {@link DEFAULT_MCP_CALL_TIMEOUT_MS} (2 min).
|
||||
*
|
||||
* Catches a tool that keeps the connection warm (SSE heartbeats / trickle) but
|
||||
* never returns a result — which the transport silence timeout
|
||||
|
||||
@@ -21,10 +21,24 @@ import { recordHttpResponse } from './integrations/metrics/http-metrics.hook';
|
||||
import { startMetricsServer } from './integrations/metrics/metrics.server';
|
||||
|
||||
async function bootstrap() {
|
||||
// Fastify JSON body cap. Fastify defaults to 1 MiB, which a long AI-chat
|
||||
// research turn exceeds: the client resends the FULL message history (every
|
||||
// tool call + search result) on each turn, so a deep conversation's POST to
|
||||
// /api/ai-chat/stream can be several MB and would otherwise be rejected with
|
||||
// FST_ERR_CTP_BODY_TOO_LARGE (413). Raise the cap; override with
|
||||
// HTTP_JSON_BODY_LIMIT (bytes). A missing/invalid/non-positive value keeps the
|
||||
// 25 MiB default. Multipart uploads are unaffected (their own @fastify/multipart
|
||||
// limits apply); this only bounds JSON/urlencoded request bodies.
|
||||
const bodyLimitEnv = Number(process.env.HTTP_JSON_BODY_LIMIT);
|
||||
const bodyLimit =
|
||||
Number.isFinite(bodyLimitEnv) && bodyLimitEnv > 0
|
||||
? bodyLimitEnv
|
||||
: 25 * 1024 * 1024;
|
||||
const app = await NestFactory.create<NestFastifyApplication>(
|
||||
AppModule,
|
||||
new FastifyAdapter({
|
||||
trustProxy: resolveTrustProxy(process.env.TRUST_PROXY),
|
||||
bodyLimit,
|
||||
routerOptions: {
|
||||
maxParamLength: 1000,
|
||||
ignoreTrailingSlash: true,
|
||||
|
||||
Reference in New Issue
Block a user