alibaba/canal · error · RuntimeException

download failed , url:{} , statusCode:{}

Error message

download failed , url:{} , statusCode:{}

What it means

Thrown as RuntimeException from the RDS binlog download path when the HTTP GET to the RDS binlog download link returns a non-200 status code. The exception includes the download URL and the HTTP status code. This is the per-file download failure — the RDS API provided a pre-signed download URL, but fetching the actual binlog file content failed.

Source

Thrown at parse/src/main/java/com/alibaba/otter/canal/parse/inbound/mysql/rds/BinlogDownloadQueue.java:203

                    .register("https", new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE))
                    .build()))
                .build();
        } else {
            httpClient = HttpClientBuilder.create().setMaxConnPerRoute(50).setMaxConnTotal(100).build();
        }

        try {
            HttpGet httpGet = new HttpGet(downloadLink);
            RequestConfig requestConfig = RequestConfig.custom()
                .setConnectTimeout(TIMEOUT)
                .setConnectionRequestTimeout(TIMEOUT)
                .setSocketTimeout(TIMEOUT)
                .build();
            httpGet.setConfig(requestConfig);
            HttpResponse response = httpClient.execute(httpGet);
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode != HttpResponseStatus.OK.code()) {
                throw new RuntimeException("download failed , url:" + downloadLink + " , statusCode:" + statusCode);
            }
            saveFile(new File(destDir), "mysql-bin." + fileName, response);
        } finally {
            httpClient.close();
        }
    }

    private static void saveFile(File parentFile, String fileName, HttpResponse response) throws IOException {
        InputStream is = response.getEntity().getContent();
        boolean isChunked = response.getEntity().isChunked();
        Header contentLengthHeader = response.getFirstHeader("Content-Length");
        long totalSize = (isChunked || contentLengthHeader == null) ? 0 : Long.parseLong(contentLengthHeader.getValue());
        if (response.getFirstHeader("Content-Disposition") != null) {
            fileName = response.getFirstHeader("Content-Disposition").getValue();
            fileName = StringUtils.substringAfter(fileName, "filename=");
        }
        boolean isTar = StringUtils.endsWith(fileName, ".tar");
        FileUtils.forceMkdir(parentFile);

View on GitHub (pinned to 87be50e876)

Solutions

  1. Check the status code: 403 = expired URL (re-request from RDS API), 404 = file gone, 429 = rate limit, 5xx = server error.
  2. Increase the TIMEOUT constant in BinlogDownloadQueue if downloads are timing out on large binlog files.
  3. Implement retry logic with exponential backoff around the download call for transient 5xx/429 errors.
  4. If 403 persists, verify the AccessKey/SecretKey are valid and not expired.

Example fix

// before — single attempt, no retry
BinlogFile binlogFile = downloadQueue.tryOne();

// after — retry with backoff for transient failures
int maxRetries = 3;
for (int i = 0; i < maxRetries; i++) {
    try {
        BinlogFile binlogFile = downloadQueue.tryOne();
        break;
    } catch (RuntimeException e) {
        if (i == maxRetries - 1) throw e;
        Thread.sleep((long) Math.pow(2, i) * 1000);
    }
}
Defensive patterns

Strategy: retry

Try / catch

int maxRetries = 3;
for (int attempt = 0; attempt < maxRetries; attempt++) {
    try {
        BinlogFile file = downloadQueue.tryOne();
        break;
    } catch (RuntimeException e) {
        if (e.getMessage().contains("download failed")) {
            int statusCode = extractStatusCode(e.getMessage());
            if (statusCode == 403) {
                // URL expired — re-request from RDS API
                refreshDownloadLinks();
            } else if (statusCode >= 500 && attempt < maxRetries - 1) {
                Thread.sleep((long) Math.pow(2, attempt) * 1000);
                continue;
            }
        }
        throw e;
    }
}

Prevention

When it happens

Trigger: httpClient.execute(httpGet) returns an HttpResponse whose status code is not 200 (HttpResponseStatus.OK.code()). Common non-200 codes: 403 (expired or invalid pre-signed URL), 404 (binlog file removed), 408/504 (timeout), 429 (rate limited), 500/502/503 (RDS storage backend error).

Common situations: The pre-signed download URL expired before the download started (RDS URLs have a short TTL). RDS rate-limited the download request. The RDS storage backend had a transient error. Network issues between Canal and the RDS OSS/storage endpoint. The binlog file was purged between the API listing and the actual download.

Related errors


AI-assisted analysis of alibaba/canal@87be50e876 (2026-08-14). Data as JSON: /api/errors/f5cc689729b32545. Report an issue: GitHub.