From 618755c9b4cc58fa43b464967253f53f38629fc2 Mon Sep 17 00:00:00 2001 From: Ashfaqbs <105435085+Ashfaqbs@users.noreply.github.com> Date: Fri, 25 Sep 2026 08:56:15 +0530 Subject: [PATCH] fix(client): keep status and body when a 422 is not a validation error A 422 whose body cannot be parsed as a validation error (for example an HTML page from a gateway) surfaced as a raw Jackson parse exception and lost the status code and response body. Fall back to DoclingServeClientException, as for any other 4xx/5xx. Signed-off-by: Ashfaqbs <105435085+Ashfaqbs@users.noreply.github.com> --- .../serve/client/DoclingServeClient.java | 17 +++++- .../DoclingServeClientErrorResponseTests.java | 57 +++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 docling-serve/docling-serve-client/src/test/java/ai/docling/serve/client/DoclingServeClientErrorResponseTests.java diff --git a/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/DoclingServeClient.java b/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/DoclingServeClient.java index faffaa2a..2704db4a 100644 --- a/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/DoclingServeClient.java +++ b/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/DoclingServeClient.java @@ -347,8 +347,9 @@ protected T getResponse(HttpRequest request, HttpResponse response, Class } } - if (statusCode == 422) { - var validationError = readValue(body.toString(), ValidationError.class); + var validationError = statusCode == 422 ? parseValidationError(body.toString()) : null; + + if (validationError != null) { var errorText = validationError.getErrorDetails() .stream() .map(ValidationErrorDetail::getMessage) @@ -376,6 +377,18 @@ protected T getResponse(HttpRequest request, HttpResponse response, Class } } + // A 422 from something other than docling-serve (e.g. a gateway) may not carry a validation body; + // fall back to the generic error so the status code and body are not lost to a parse failure. + private @Nullable ValidationError parseValidationError(String body) { + try { + return readValue(body, ValidationError.class); + } + catch (RuntimeException e) { + LOG.debug("422 response body is not a validation error", e); + return null; + } + } + @Override public HealthCheckResponse health() { return this.healthOps.health(); diff --git a/docling-serve/docling-serve-client/src/test/java/ai/docling/serve/client/DoclingServeClientErrorResponseTests.java b/docling-serve/docling-serve-client/src/test/java/ai/docling/serve/client/DoclingServeClientErrorResponseTests.java new file mode 100644 index 00000000..c4ba507d --- /dev/null +++ b/docling-serve/docling-serve-client/src/test/java/ai/docling/serve/client/DoclingServeClientErrorResponseTests.java @@ -0,0 +1,57 @@ +package ai.docling.serve.client; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; + +import ai.docling.serve.api.DoclingServeApi; +import ai.docling.serve.api.validation.ValidationException; + +class DoclingServeClientErrorResponseTests { + @RegisterExtension + static WireMockExtension wireMock = WireMockExtension.newInstance() + .options(wireMockConfig().dynamicPort()) + .build(); + + private DoclingServeApi client() { + return DoclingServeApi.builder() + .baseUrl("http://localhost:%d".formatted(wireMock.getRuntimeInfo().getHttpPort())) + .build(); + } + + @Test + void unprocessableEntityWithValidationBodyThrowsValidationException() { + wireMock.stubFor(get(urlPathEqualTo("/health")) + .willReturn(aResponse() + .withStatus(422) + .withHeader("Content-Type", "application/json") + .withBody("{\"detail\": [{\"type\": \"missing\", \"loc\": [\"body\"], \"msg\": \"Field required\"}]}"))); + + assertThatThrownBy(() -> client().health()) + .isInstanceOf(ValidationException.class) + .hasMessageContaining("Field required"); + } + + @Test + void unprocessableEntityWithNonValidationBodyKeepsStatusAndBody() { + wireMock.stubFor(get(urlPathEqualTo("/health")) + .willReturn(aResponse() + .withStatus(422) + .withHeader("Content-Type", "text/html") + .withBody("Unprocessable by gateway"))); + + assertThatThrownBy(() -> client().health()) + .isInstanceOfSatisfying(DoclingServeClientException.class, e -> { + assertThat(e.getStatusCode()).isEqualTo(422); + assertThat(e.getResponseBody()).isEqualTo("Unprocessable by gateway"); + }); + } +}