From edac98b7f50ed9a4c32978c8a67388fa33003342 Mon Sep 17 00:00:00 2001 From: sqersters <109853788+bouclem@users.noreply.github.com> Date: Tue, 26 May 2026 15:02:19 +0200 Subject: [PATCH] fix: add download timeouts and retry on transient failures Fixes #242. Three downloader weaknesses combined to make setup fragile when a remote (e.g. vault.omniarchive.uk) is slow or flaky: - openURLStream did not set connect or read timeouts, so a stalled remote could hang the GUI indefinitely (user reported being stuck at 5%) - downloadFile only attempted once, so any single SSL reset or timeout aborted the whole setup - A failed mid-stream download left a partial file on disk that could confuse subsequent runs Set 30s connect / 60s read timeouts, retry up to 3 times with linear backoff on IOException, and clean up the partial file before each retry. Also closes the URL stream via try-with-resources, fixing a small resource leak in the original. --- .../org/mcphackers/mcp/tools/FileUtil.java | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/mcphackers/mcp/tools/FileUtil.java b/src/main/java/org/mcphackers/mcp/tools/FileUtil.java index 80f39b8..5c3aff3 100644 --- a/src/main/java/org/mcphackers/mcp/tools/FileUtil.java +++ b/src/main/java/org/mcphackers/mcp/tools/FileUtil.java @@ -140,16 +140,44 @@ public static void downloadFile(String url, Path output) throws IOException { } public static void downloadFile(URL url, Path output) throws IOException { - ReadableByteChannel channel = Channels.newChannel(openURLStream(url)); - try (FileOutputStream stream = new FileOutputStream(output.toAbsolutePath().toString())) { - FileChannel fileChannel = stream.getChannel(); - fileChannel.transferFrom(channel, 0, Long.MAX_VALUE); + final int maxAttempts = 3; + IOException lastError = null; + for (int attempt = 1; attempt <= maxAttempts; attempt++) { + try (InputStream in = openURLStream(url)) { + ReadableByteChannel channel = Channels.newChannel(in); + try (FileOutputStream stream = new FileOutputStream(output.toAbsolutePath().toString())) { + FileChannel fileChannel = stream.getChannel(); + fileChannel.transferFrom(channel, 0, Long.MAX_VALUE); + } + return; + } catch (IOException e) { + lastError = e; + // Don't leave a half-written file behind for the next attempt or + // for the SHA1 verification on subsequent runs. + try { + Files.deleteIfExists(output); + } catch (IOException ignored) { + } + if (attempt < maxAttempts) { + try { + Thread.sleep(1000L * attempt); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw e; + } + } + } } + throw lastError; } public static InputStream openURLStream(URL url) throws IOException { URLConnection connection = url.openConnection(); connection.setRequestProperty("User-Agent", "RetroMCP/" + MCP.VERSION); + // Without explicit timeouts, a stalled remote (slow archive mirror, dropped + // packet, blackholed route) hangs the download indefinitely. + connection.setConnectTimeout(30_000); + connection.setReadTimeout(60_000); return connection.getInputStream(); }