{"record":{"id":"66919f32748b242d","repo":"opendataloader-project/opendataloader-pdf","slug":"failed-to-convert-via-hancom-ai","errorCode":null,"errorMessage":"Failed to convert via Hancom AI","messagePattern":"Failed to convert via Hancom AI","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"java/opendataloader-pdf-core/src/main/java/org/opendataloader/pdf/hybrid/HancomAIClient.java","lineNumber":276,"sourceCode":"    }\n\n    /**\n     * Creates a PageImageCache based on config.\n     */\n    private PageImageCache createPageImageCache() throws IOException {\n        if (\"disk\".equalsIgnoreCase(config.getImageCache())) {\n            return new DiskPageImageCache();\n        }\n        return new MemoryPageImageCache();\n    }\n\n    @Override\n    public CompletableFuture<HybridResponse> convertAsync(HybridRequest request) {\n        return CompletableFuture.supplyAsync(() -> {\n            try {\n                return convert(request);\n            } catch (IOException e) {\n                throw new IllegalStateException(\"Failed to convert via Hancom AI\", e);\n            }\n        });\n    }\n\n    /**\n     * Captions each Figure found by DLA:\n     * 1. Get page images via pdf2img\n     * 2. Find Figure objects (label 10) from DLA results\n     * 3. Crop each Figure from page image\n     * 4. Send cropped image to IMAGE_CAPTIONING\n     *\n     * @return ArrayNode of {page_number, object_id, bbox, caption}\n     */\n    private ArrayNode captionFigures(byte[] pdfBytes, JsonNode dlaResult,\n                                     PageImageCache pageImageCache, CropOutput cropOutput) {\n        ArrayNode captions = objectMapper.createArrayNode();\n\n        // Extract pages from DLA result","sourceCodeStart":258,"sourceCodeEnd":294,"githubUrl":"https://github.com/opendataloader-project/opendataloader-pdf/blob/a7789b8e77dd05e2b8659eb3ea12fc458f80bfb8/java/opendataloader-pdf-core/src/main/java/org/opendataloader/pdf/hybrid/HancomAIClient.java#L258-L294","documentation":"convertAsync runs convert() on a CompletableFuture; checked IOException cannot propagate out of supplyAsync, so it is wrapped as IllegalStateException with the real IOException as cause. This is the Hancom AI equivalent of error 45 — the message is generic ('Failed to convert via Hancom AI') because the cause carries the actionable detail (DLA empty, pdf2img failure, etc.).","triggerScenarios":"Calling HancomAIClient.convertAsync(request) and the underlying convert() throws IOException (DLA empty result, pdf2img HTTP error, page image fetch failure that escapes per-page handling, etc.).","commonSituations":"Using the async API and not inspecting getCause(); backend becoming unavailable mid-pipeline after the health check passed; a page-image failure that is not caught by the per-page handlers.","solutions":["Inspect the cause: ((IllegalStateException)e).getCause() for the real IOException","Prefer synchronous convert() if you handle checked IOException directly","Call checkAvailability() before convertAsync to fail fast","Add retry with backoff for transient backend failures"],"exampleFix":"// before\nHybridResponse r = client.convertAsync(req).join();\n// after\nHybridResponse r = client.convertAsync(req)\n    .exceptionally(e -> {\n        Throwable c = (e instanceof CompletionException && e.getCause() != null) ? e.getCause() : e;\n        throw new RuntimeException(\"hancom convert failed: \" + c.getMessage(), c);\n    }).join();","handlingStrategy":"try-catch","validationCode":"// Fail fast before the async call\nclient.checkAvailability();","typeGuard":null,"tryCatchPattern":"client.convertAsync(request)\n    .exceptionally(e -> {\n        Throwable cause = (e instanceof CompletionException && e.getCause() != null)\n            ? e.getCause() : e;\n        // cause is the real IOException\n        log.error(\"Hancom convert failed: {}\", cause.getMessage(), cause);\n        return null; // or trigger Java fallback\n    });","preventionTips":["Always unwrap getCause() from convertAsync exceptions","Prefer synchronous convert() to handle checked IOException directly","Call checkAvailability() first for a precise failure reason"],"tags":["hybrid","hancom-ai","async","completable-future"],"backgroundTag":null,"analyzedSha":"a7789b8e77dd05e2b8659eb3ea12fc458f80bfb8","analyzedAt":"2026-08-14T05:22:03.953Z","schemaVersion":2},"datasetVersion":"2026-08-14T10:17:34.591Z"}