opendataloader-project/opendataloader-pdf · error · IllegalStateException
Failed to convert
Error message
Failed to convert
What it means
Inside convertAsync(), the checked IOException from the synchronous convert() method is caught and wrapped in an unchecked IllegalStateException with the message 'Failed to convert'. This is a deliberate bridge: CompletableFuture.supplyAsync takes a Supplier (which cannot throw checked exceptions), so the IOException must be hidden. Callers that .join() or .get() the future will receive a java.util.concurrent.CompletionException whose cause is this IllegalStateException whose cause is the original IOException.
Source
Thrown at java/opendataloader-pdf-core/src/main/java/org/opendataloader/pdf/hybrid/HancomClient.java:156
JsonNode visualInfo = getVisualInfo(fileId);
LOGGER.log(Level.FINE, "Retrieved visual info for file: {0}", fileId);
return new HybridResponse(null, visualInfo, null);
} finally {
// Step 3: Always cleanup
if (fileId != null) {
deleteFile(fileId);
}
}
}
@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;
}
/**
* Uploads a PDF file to the Hancom API.
*
* @param pdfBytes The PDF file bytes.
* @return The file ID assigned by the server.View on GitHub (pinned to a7789b8e77)
Solutions
- When calling .join() on the returned CompletableFuture, catch CompletionException and unwrap: `catch (CompletionException e) { Throwable cause = e.getCause(); if (cause instanceof IllegalStateException) cause = cause.getCause(); /* now the original IOException */ }`.
- Switch to the synchronous convert() method directly if you don't need async — it throws IOException directly with the original diagnostic message.
- Use .handle() or .exceptionally() on the future to intercept and unwrap the exception before it propagates.
- Check the root cause's message (two levels down) for the real backend error details.
Example fix
// before: CompletionException with buried cause
HybridResponse resp = client.convertAsync(request).join();
// after: unwrap to original IOException
HybridResponse resp;
try {
resp = client.convertAsync(request).join();
} catch (CompletionException e) {
Throwable cause = e.getCause();
if (cause instanceof IllegalStateException) {
cause = cause.getCause(); // original IOException
}
throw new IOException("Async conversion failed: " + cause.getMessage(), cause);
} Defensive patterns
Strategy: try-catch
Validate before calling
// Prefer the synchronous API when possible to avoid exception wrapping
// If async is needed, validate inputs before calling convertAsync:
if (request.getPdfBytes() == null || request.getPdfBytes().length == 0) {
throw new IllegalArgumentException("PDF bytes cannot be null or empty");
} Try / catch
try {
HybridResponse resp = client.convertAsync(request).join();
} catch (CompletionException e) {
Throwable cause = e.getCause();
if (cause instanceof IllegalStateException) {
cause = cause.getCause(); // unwrap to original IOException
}
if (cause instanceof IOException) {
// handle the real backend error
LOGGER.severe("Conversion failed: " + cause.getMessage());
}
} Prevention
- Always unwrap CompletionException → IllegalStateException → IOException when handling async failures.
- Prefer the synchronous convert() method when you don't need parallelism — it throws IOException directly.
- Use .exceptionally() on the CompletableFuture to centralize error handling.
- Log the root cause message, not just the wrapper's 'Failed to convert'.
When it happens
Trigger: Calling HancomClient.convertAsync(request) and then .join(), .get(), or passing the future to a downstream stage that triggers execution. Any IOException during upload, visualinfo retrieval, or cleanup (deleteFile) will surface as this wrapping exception. Specifically: upload HTTP failure (error 63), empty upload body (error 64), malformed JSON (errors 65-66), visualinfo failure (errors 67-68), or network timeout.
Common situations: Using the async API in a parallel pipeline (e.g., processing multiple pages concurrently with ForkJoinPool) and not unwrapping the CompletionException; the original IOException's diagnostic message (e.g., 'Hancom upload failed with status 500: ...') is buried two levels deep in the cause chain and easy to miss.
Related errors
- Failed to convert
- Failed to convert via Hancom AI
- Hybrid server is not available at %s Please check the server
- Hancom upload failed with status %s: %s
- Empty response body from upload
AI-assisted analysis of opendataloader-project/opendataloader-pdf@a7789b8e77 (2026-08-14).
Data as JSON: /api/errors/ac1309fe13f4de50.
Report an issue: GitHub.