opendataloader-project/opendataloader-pdf · error · IllegalStateException

Failed to convert

Error message

Failed to convert

What it means

convertAsync runs convert() on a CompletableFuture; checked IOException cannot propagate out of supplyAsync, so it is wrapped as IllegalStateException with cause set to the real IOException. The wrapped cause (any of the docling convert/parse failures) holds the actionable detail, and the message 'Failed to convert' is intentionally generic because the cause message carries the specifics.

Source

Thrown at java/opendataloader-pdf-core/src/main/java/org/opendataloader/pdf/hybrid/DoclingFastServerClient.java:149

    }

    @Override
    public HybridResponse convert(HybridRequest request) throws IOException {
        Request httpRequest = buildConvertRequest(request);
        LOGGER.log(Level.FINE, "Sending request to {0}", baseUrl + CONVERT_ENDPOINT);

        try (Response response = httpClient.newCall(httpRequest).execute()) {
            return parseResponse(response);
        }
    }

    @Override
    public CompletableFuture<HybridResponse> convertAsync(HybridRequest request) {
        return CompletableFuture.supplyAsync(() -> {
            try {
                return convert(request);
            } catch (IOException e) {
                throw new IllegalStateException("Failed to convert", e);
            }
        });
    }

    /**
     * Gets the base URL of this client.
     *
     * @return The base URL.
     */
    public String getBaseUrl() {
        return baseUrl;
    }

    /**
     * Builds a multipart/form-data HTTP request for the convert endpoint.
     */
    private Request buildConvertRequest(HybridRequest request) {
        MultipartBody.Builder bodyBuilder = new MultipartBody.Builder()

View on GitHub (pinned to a7789b8e77)

Solutions

  1. Inspect the exception chain: future.exceptionally(e -> ...) and read ((IllegalStateException)e).getCause() for the real IOException
  2. Prefer the synchronous convert() if you can handle checked IOException directly
  3. Add retry/backoff around convertAsync for transient failures
  4. Call checkAvailability() before convertAsync to fail fast with a clearer message

Example fix

// before
CompletableFuture<HybridResponse> f = client.convertAsync(req);
HybridResponse r = f.join(); // throws bare IllegalStateException
// after
HybridResponse r = client.convertAsync(req)
    .exceptionally(e -> {
        Throwable cause = (e instanceof CompletionException && e.getCause() != null) ? e.getCause() : e;
        throw new RuntimeException("convert failed: " + cause.getMessage(), cause);
    }).join();
Defensive patterns

Strategy: try-catch

Validate before calling

// Fail fast with a clearer message before the async call
client.checkAvailability(); // throws a precise IOException if unreachable

Try / catch

client.convertAsync(request)
    .whenComplete((resp, ex) -> {
        if (ex != null) {
            Throwable cause = (ex instanceof CompletionException && ex.getCause() != null)
                ? ex.getCause() : ex;
            // cause is the real IOException (IllegalStateException wraps it)
            log.error("convert failed: {}", cause.getMessage(), cause);
        }
    });

Prevention

When it happens

Trigger: Calling DoclingFastServerClient.convertAsync(request) and the underlying convert() throws IOException (network failure on /v1/convert/file, non-2xx status, malformed response, missing document field, etc.).

Common situations: Using the async API and not inspecting getCause(); the server becoming unavailable mid-run; transient network errors surfacing only on the convert call after health check passed.

Related errors


AI-assisted analysis of opendataloader-project/opendataloader-pdf@a7789b8e77 (2026-08-14). Data as JSON: /api/errors/4187cc3827aa2654. Report an issue: GitHub.