No request timeout - a stalled Forgejo request hangs the tool call forever #3

Closed
opened 2026-07-29 05:58:38 +00:00 by trtmn · 0 comments
Owner

Problem

forgejoRequest in src/forgejo-client.js calls fetch(url, { method, headers, body }) with no signal. Node has no default fetch timeout, so if the instance stops responding mid-request (Cloudflare in front of git.trtmn.io, Tailscale route flapping, a hung upstream) the promise never settles. The MCP tool call hangs indefinitely with no error surfaced to the caller.

This compounds in src/issue-helpers.js: fetchAllPages loops up to MAX_PAGES (10) sequential requests, so create_issue can issue up to 20 requests (labels plus milestones) with no per-request bound.

Suggested fix

Add a bounded, overridable timeout:

const TIMEOUT_MS = Number(process.env.FORGEJO_TIMEOUT_MS ?? 30000);

const res = await fetch(url, {
  method,
  headers,
  body: payload,
  signal: AbortSignal.timeout(TIMEOUT_MS),
});

Wrap the call so an abort surfaces as a clear message rather than a bare TimeoutError, e.g. Forgejo request timed out after 30000ms: GET /repos/.... Make sure the error text never includes the Authorization header or token.

Also worth considering

fetch rejects on network errors (DNS failure, connection refused) and JSON.parse can throw on a malformed body that is nonetheless labelled application/json. Both currently propagate as raw exceptions out of the tool handler. Catching them in forgejoRequest and returning a structured { ok: false, status: 0, data: message } would give callers consistent shape.

Severity

Medium - reliability. No data loss, but a hung tool call is indistinguishable from a stuck session.

## Problem `forgejoRequest` in `src/forgejo-client.js` calls `fetch(url, { method, headers, body })` with no `signal`. Node has no default fetch timeout, so if the instance stops responding mid-request (Cloudflare in front of git.trtmn.io, Tailscale route flapping, a hung upstream) the promise never settles. The MCP tool call hangs indefinitely with no error surfaced to the caller. This compounds in `src/issue-helpers.js`: `fetchAllPages` loops up to `MAX_PAGES` (10) sequential requests, so `create_issue` can issue up to 20 requests (labels plus milestones) with no per-request bound. ## Suggested fix Add a bounded, overridable timeout: ```js const TIMEOUT_MS = Number(process.env.FORGEJO_TIMEOUT_MS ?? 30000); const res = await fetch(url, { method, headers, body: payload, signal: AbortSignal.timeout(TIMEOUT_MS), }); ``` Wrap the call so an abort surfaces as a clear message rather than a bare `TimeoutError`, e.g. `Forgejo request timed out after 30000ms: GET /repos/...`. Make sure the error text never includes the Authorization header or token. ## Also worth considering `fetch` rejects on network errors (DNS failure, connection refused) and `JSON.parse` can throw on a malformed body that is nonetheless labelled `application/json`. Both currently propagate as raw exceptions out of the tool handler. Catching them in `forgejoRequest` and returning a structured `{ ok: false, status: 0, data: message }` would give callers consistent shape. ## Severity Medium - reliability. No data loss, but a hung tool call is indistinguishable from a stuck session.
trtmn 2026-07-29 14:06:51 +00:00
  • closed this issue
  • added the
    CR2
    P2
    labels
Sign in to join this conversation.
No description provided.