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 4654f89..eed1866 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 @@ -89,15 +89,19 @@ public abstract class DoclingServeClient extends HttpOperations implements Docli private final TaskOperations taskOps; protected DoclingServeClient(DoclingServeClientBuilder builder) { - this.baseUrl = ensureNotNull(builder.baseUrl, "baseUrl"); + var base = ensureNotNull(builder.baseUrl, "baseUrl"); - if (Objects.equals(this.baseUrl.getScheme(), "http")) { + if (Objects.equals(base.getScheme(), "http")) { // Docling Serve uses Python FastAPI which causes errors when called from JDK HttpClient. // The HttpClient uses HTTP 2 by default and then falls back to HTTP 1.1 if not supported. // However, the way FastAPI works results in the fallback not happening, making the call fail. builder.httpClientBuilder.version(HttpClient.Version.HTTP_1_1); } + this.baseUrl = !base.getPath().endsWith("/") ? + URI.create(base + "/") : + base; + this.httpClient = ensureNotNull(builder.httpClientBuilder, "httpClientBuilder").build(); this.logRequests = builder.logRequests; this.logResponses = builder.logResponses; @@ -229,7 +233,7 @@ protected O executeGet(RequestContext requestContext) { protected HttpRequest.Builder createRequestBuilder(RequestContext requestContext) { var requestBuilder = HttpRequest.newBuilder() - .uri(this.baseUrl.resolve(requestContext.getUri())) + .uri(this.baseUrl.resolve(resolvePath(requestContext.getUri()))) .header("Accept", "application/json"); if (Utils.isNotNullOrBlank(this.apiKey)) { @@ -239,6 +243,13 @@ protected HttpRequest.Builder createRequestBuilder(RequestContext r return requestBuilder; } + private String resolvePath(String path) { + return Optional.ofNullable(path) + .filter(p -> p.startsWith("/") && (p.length() > 1)) + .map(p -> p.substring(1)) + .orElse(path); + } + protected T getResponse(HttpRequest request, HttpResponse response, Class expectedReturnType) { var body = response.body(); diff --git a/docling-serve/docling-serve-client/src/test/java/ai/docling/serve/client/AbstractDoclingServeClientTests.java b/docling-serve/docling-serve-client/src/test/java/ai/docling/serve/client/AbstractDoclingServeClientTests.java index 51b49c4..d6acde3 100644 --- a/docling-serve/docling-serve-client/src/test/java/ai/docling/serve/client/AbstractDoclingServeClientTests.java +++ b/docling-serve/docling-serve-client/src/test/java/ai/docling/serve/client/AbstractDoclingServeClientTests.java @@ -2,11 +2,15 @@ import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.matchingJsonPath; import static com.github.tomakehurst.wiremock.client.WireMock.okJson; import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -43,6 +47,7 @@ import org.slf4j.LoggerFactory; import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; import ai.docling.core.DoclingDocument; import ai.docling.core.DoclingDocument.DocItemLabel; @@ -107,13 +112,13 @@ public void testFailed(ExtensionContext context, @Nullable Throwable cause) { } }; -// @RegisterExtension -// static WireMockExtension wireMockExtension = WireMockExtension.newInstance() -// .options(wireMockConfig().dynamicPort()) -// .configureStaticDsl(true) -// .failOnUnmatchedRequests(true) -// .resetOnEachTest(true) -// .build(); + @RegisterExtension + static WireMockExtension wireMockExtension = WireMockExtension.newInstance() + .options(wireMockConfig().dynamicPort()) + .configureStaticDsl(true) + .failOnUnmatchedRequests(true) + .resetOnEachTest(true) + .build(); static { doclingContainer.start(); @@ -155,6 +160,37 @@ void builderWorks() { .isInstanceOf(DoclingServeClient.class); } + @Test + void pathPartFromBaseUrl() { + // From https://github.com/docling-project/docling-java/issues/294 + var wiremockRuntimeInfo = wireMockExtension.getRuntimeInfo(); + wireMockExtension.stubFor( + get(urlPathEqualTo("/path/health")) + .withHeader("Accept", equalTo("application/json")) + .willReturn(okJson("{\"status\": \"ok\"}")) + ); + + var baseUrl = "http://localhost:%d/path".formatted(wiremockRuntimeInfo.getHttpPort()); + + var client = DoclingServeApi.builder() + .logRequests() + .logResponses() + .prettyPrint() + .baseUrl(baseUrl) + .build(); + + assertThat(client.health()) + .isNotNull() + .extracting(HealthCheckResponse::getStatus) + .isEqualTo("ok"); + + verify( + 1, + getRequestedFor(urlPathEqualTo("/path/health")) + .withHeader("Accept", equalTo("application/json")) + ); + } + @Nested class ClearTests { @Test