Repository navigation
fix(client): report the error message of a failed async task #776
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,7 @@ | |
| import ai.docling.serve.api.task.request.TaskResultRequest; | ||
| import ai.docling.serve.api.task.request.TaskStatusPollRequest; | ||
| import ai.docling.serve.api.task.response.TaskStatusPollResponse; | ||
| import ai.docling.serve.api.util.Utils; | ||
| import ai.docling.serve.api.util.ValidationUtils; | ||
|
|
||
| /** | ||
|
|
@@ -128,8 +129,11 @@ private <O> CompletionStage<O> pollTaskStatus(TaskStatusPollResponse statusRespo | |
| } | ||
|
|
||
| case FAILURE -> { | ||
| var errorMessage = Optional.ofNullable(statusResponse.getTaskStatusMetadata()) | ||
| .map(metadata -> "Task failed: %s".formatted(metadata)) | ||
| // Prefer the reason reported by Docling Serve, as the task metadata only holds document counts | ||
| var errorMessage = Optional.ofNullable(statusResponse.getErrorMessage()) | ||
| .filter(Utils::isNotNullOrBlank) | ||
| .or(() -> Optional.ofNullable(statusResponse.getTaskStatusMetadata()).map(Object::toString)) | ||
| .map(reason -> "Task failed: %s".formatted(reason)) | ||
| .orElse("Task failed"); | ||
|
Comment on lines
+132
to
137
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Flagging this so you don't wonder whether I skipped it: it's correct as written, including dropping the I'd actually drafted a comment asking you to keep the counts, on the theory that a failed batch would be reporting something like "3 of 10 documents failed." It isn't. |
||
|
|
||
| yield CompletableFuture.failedStage( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -155,6 +155,38 @@ void failedTaskRunsEveryPollOnCustomExecutor() { | |
| getWireMock().verify(0, getRequestedFor(urlPathEqualTo(RESULT_PATH))); | ||
| } | ||
|
|
||
| @Test | ||
| void failedTaskReportsTheErrorMessageOfTheServer() { | ||
| getWireMock().stubFor( | ||
| post(urlPathEqualTo("/v1/convert/source/async")) | ||
| .willReturn(okJson(taskStatus("pending"))) | ||
| ); | ||
|
|
||
| getWireMock().stubFor( | ||
| get(urlPathEqualTo(POLL_PATH)) | ||
| .willReturn(okJson(""" | ||
| { | ||
| "task_id": "%s", | ||
| "task_status": "failure", | ||
| "task_meta": { | ||
| "num_docs": 1, | ||
| "num_processed": 1, | ||
| "num_succeeded": 0, | ||
| "num_failed": 1 | ||
| }, | ||
| "error_message": "Input document is not valid" | ||
| } | ||
| """.formatted(TASK_ID))) | ||
| ); | ||
|
Comment on lines
+165
to
+180
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This stub covers the case the issue describes, which is the right one to lead with. Could you add four more variations on it? Together they pin the whole decision tree in the To be clear on where these go: they belong in the new
|
||
|
|
||
| var future = client(null) | ||
| .convertSourceAsync(convertRequest()) | ||
| .toCompletableFuture(); | ||
|
|
||
| assertThatThrownBy(() -> future.get(10, TimeUnit.SECONDS)) | ||
| .hasRootCauseMessage("Async conversion failed for task %s: Task failed: Input document is not valid".formatted(TASK_ID)); | ||
| } | ||
|
Comment on lines
+158
to
+188
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good call adding coverage rather than trusting the change — one request though, on where it lives. Could you move it into a new
This class is a narrower thing — it's all about the
Two nearby classes look like candidates but aren't, so you don't lose time on the detour: |
||
|
|
||
| @Test | ||
| void asyncOperationsWorkWithoutCustomExecutor() throws Exception { | ||
| stubTask("/v1/convert/source/async", "success"); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -26,6 +26,7 @@ Docling Java {{ gradle.project_version }} includes important breaking changes, a | |||||
|
|
||||||
| ### {{ gradle.project_version }} | ||||||
|
|
||||||
| * **A failed async task reports the reason given by Docling Serve** — When an async conversion or chunking task failed, the exception only showed the task metadata, which holds document counts, so the cause of the failure was lost. `TaskStatusPollResponse` now has an `errorMessage` field, mapped from the `error_message` that Docling Serve returns, and the exception message includes it when it is present. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The entry is accurate, but I think it will generate a bug report in a few weeks. Could you append the last sentence here?
Suggested change
The reason: |
||||||
| * **New `HeadingHierarchyOptions` and `headingHierarchyOptions` on `ConvertDocumentOptions`** — When set, `headingHierarchyOptions` enables PDF section-header level inference from bookmarks, outline numbering and visual style. The helper getter `doPdfHeadingHierarchy()` is derived from the presence of these options (returns `true` when non-null, `false` otherwise), so callers only need to pass the options to enable hierarchy inference. The options class covers all docling-serve fields (`useBookmarks`, `useNumbering`, `useStyle`, `useFontStyle`, `styleSizeTolerance`, `numberingSchemes`, `maxLevel`, `bookmarkMatchThreshold`). | ||||||
| * **Breaking: new `DoclingServeApiProvider` SPI** — Implementations are now provided through `ai.docling.serve.api.spi.DoclingServeApiProvider`, which receives an immutable `DoclingServeApiConfig`, so new options can be added without breaking implementations. A provider can declare the options it doesn't honor via `unsupportedOptions()`, to have them ignored, logged or rejected when a caller sets them. `DoclingServeApi` has a new abstract `config()` method. `DoclingServeApiBuilderFactory`, `DoclingServeApi.DoclingApiBuilder` and `DoclingServeApi.toBuilder()` are deprecated for removal; legacy factories are still used when no provider is found. `docling-serve-api` now depends on `slf4j-api`. See [Migrating to `DoclingServeApiProvider`](docling-serve/serve-api-provider-migration.md). | ||||||
| * **Breaking: `DoclingServeApi.builder()` returns a `DoclingServeApiBuilder`** — It used to return the builder of the implementation through a generic `<T, B> B` return type. Fluent chains and `var` declarations are unaffected, but code declaring the builder type must be updated. Client-specific settings such as `httpClientBuilder` are available from `DoclingServeClient.builder()`, which detects the version of Jackson the same way, and the JSON mapper from `DoclingServeJackson3Client.builder()` / `DoclingServeJackson2Client.builder()`, which are now public. An API can be copied with `api.config().toBuilder()`. | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Field shape is right, and I want to say so explicitly because the alternative is tempting. Nullable
Stringplus@Nullablematches the four fields already on this type; returningOptionalwould follow our general "Optional over null" guidance but would make this one field behave unlike its neighbors inside a Lombok wire-format DTO. Consistency is the better call here.And on what's deliberately not here, so it doesn't look like an oversight: upstream also carries
failure: PublicFailureInfo(category/message/retryable/phase). Leaving it out is correct for this PR — it's Ray-only and v1.25.0+, whileerror_messageworks everywhere we support. I've opened #778 for it, which also captures an unknown-enum-value policy question we need to settle before addingFailureCategoryas a Java enum.