From 94e50dd4d05374669a1be2d971bb3b70ef96a5dc Mon Sep 17 00:00:00 2001 From: yumakakuya Date: Mon, 4 May 2026 04:48:09 +0900 Subject: [PATCH] fix: web adapter hang on large responses (>100KB) + ProviderManager timeout race condition --- adapters/web/index.ts | 25 +++++++++++----- .../dev/sorted/mcphub/ProviderManager.java | 30 ++++++++++++++++++- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/adapters/web/index.ts b/adapters/web/index.ts index 7036f2f..de06582 100644 --- a/adapters/web/index.ts +++ b/adapters/web/index.ts @@ -95,19 +95,30 @@ function httpRequest( return; } let data = ''; + let truncated = false; res.on('data', (chunk: Buffer) => { + if (truncated) return; data += chunk.toString(); - // Truncate early to avoid memory issues if (data.length > MAX_BODY_CHARS * 2) { + truncated = true; res.destroy(); + resolve({ + statusCode: res.statusCode || 0, + headers: res.headers as Record, + body: data.slice(0, MAX_BODY_CHARS * 2), + finalUrl: urlStr + }); } }); - res.on('end', () => resolve({ - statusCode: res.statusCode || 0, - headers: res.headers as Record, - body: data, - finalUrl: urlStr - })); + res.on('end', () => { + if (truncated) return; + resolve({ + statusCode: res.statusCode || 0, + headers: res.headers as Record, + body: data, + finalUrl: urlStr + }); + }); }); req.on('error', reject); req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out (30s)')); }); diff --git a/java/src/main/java/dev/sorted/mcphub/ProviderManager.java b/java/src/main/java/dev/sorted/mcphub/ProviderManager.java index f009e57..8e31387 100644 --- a/java/src/main/java/dev/sorted/mcphub/ProviderManager.java +++ b/java/src/main/java/dev/sorted/mcphub/ProviderManager.java @@ -272,9 +272,14 @@ public synchronized JsonNode call(String groupId, String method, JsonNode params try { return futureResult.get(CALL_TIMEOUT_MS, TimeUnit.MILLISECONDS); } catch (java.util.concurrent.TimeoutException e) { + restartBrokenProvider(groupId); throw new IOException("Provider '" + groupId + "' call timed out after " + CALL_TIMEOUT_MS + "ms"); } catch (java.util.concurrent.ExecutionException e) { - throw new IOException("Provider '" + groupId + "' call failed: " + e.getCause().getMessage()); + String causeMsg = e.getCause() != null ? e.getCause().getMessage() : e.getMessage(); + if (causeMsg != null && causeMsg.contains("closed stdout")) { + restartBrokenProvider(groupId); + } + throw new IOException("Provider '" + groupId + "' call failed: " + causeMsg); } } } @@ -289,6 +294,29 @@ public boolean isRunning(String groupId) { // Internal // ------------------------------------------------------------------------- + /** Kill and restart a provider whose stdin/stdout sync may be broken + * (e.g., after a call timeout or stdout close). */ + private void restartBrokenProvider(String groupId) { + ProviderProcess broken = processes.remove(groupId); + if (broken != null) { + broken.process.destroy(); + log.warn("Provider group '{}' call failed — destroying and restarting", groupId); + for (GroupConfig g : groups) { + if (g.id.equals(groupId)) { + try { + start(g); + reportHealth(groupId, "running"); + confirmWithRegistry(g); + } catch (Exception restartEx) { + log.error("Failed to restart provider group '{}' after failure: {}", groupId, restartEx.getMessage()); + reportHealth(groupId, "unavailable"); + } + break; + } + } + } + } + private void start(GroupConfig cfg) throws IOException { // Prevent orphan processes: skip if group already has a running process ProviderProcess existing = processes.get(cfg.id);