From a97b0d4201b55d30abf7f7ffad25ace989ca0479 Mon Sep 17 00:00:00 2001 From: Thomas Cederholm Date: Wed, 20 May 2026 13:02:07 +0200 Subject: [PATCH 1/2] fix: Default basePath to / when server URL has no path URI.create("http://127.0.0.1:4444").getPath() returns an empty string, not null, so the previous Optional.ofNullable fallback left basePath empty and HttpServer.createContext rejected it. --- src/main/java/com/retailsvc/http/spec/Spec.java | 3 ++- src/test/java/com/retailsvc/http/spec/SpecTest.java | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/retailsvc/http/spec/Spec.java b/src/main/java/com/retailsvc/http/spec/Spec.java index 00bb982..4f76fc3 100644 --- a/src/main/java/com/retailsvc/http/spec/Spec.java +++ b/src/main/java/com/retailsvc/http/spec/Spec.java @@ -245,7 +245,8 @@ private static String computeBasePath(List servers) { if (servers.isEmpty()) { throw new IllegalStateException("no servers declared"); } - return Optional.ofNullable(URI.create(servers.getFirst().url()).getPath()).orElse(""); + String path = URI.create(servers.getFirst().url()).getPath(); + return (path == null || path.isEmpty()) ? "/" : path; } private static Map indexByRef(Map components, String prefix) { diff --git a/src/test/java/com/retailsvc/http/spec/SpecTest.java b/src/test/java/com/retailsvc/http/spec/SpecTest.java index 091a2bd..f3b18af 100644 --- a/src/test/java/com/retailsvc/http/spec/SpecTest.java +++ b/src/test/java/com/retailsvc/http/spec/SpecTest.java @@ -33,6 +33,18 @@ void parsesMinimalSpec() { assertThat(spec.operations()).isEmpty(); } + @Test + void basePathDefaultsToRootWhenServerUrlHasNoPath() { + Map raw = + Map.of( + "openapi", "3.1.0", + "info", Map.of("title", "x", "version", "1"), + "servers", List.of(Map.of("url", "http://127.0.0.1:4444")), + "paths", Map.of()); + Spec spec = Spec.from(raw); + assertThat(spec.basePath()).isEqualTo("/"); + } + @Test void parsesPathsWithMethods() { Map raw = From 51662c909f84f89814da61272114cce0ae36fc57 Mon Sep 17 00:00:00 2001 From: Thomas Cederholm Date: Wed, 20 May 2026 13:08:16 +0200 Subject: [PATCH 2/2] test: Verify handler dispatch when basePath is / The catch-all 404 context at / collided with the spec context when the server URL had no path. Skip the catch-all in that case. --- .../com/retailsvc/http/OpenApiServer.java | 7 +++- .../com/retailsvc/http/OpenApiServerTest.java | 39 +++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/retailsvc/http/OpenApiServer.java b/src/main/java/com/retailsvc/http/OpenApiServer.java index c7c8074..dbcbbc0 100644 --- a/src/main/java/com/retailsvc/http/OpenApiServer.java +++ b/src/main/java/com/retailsvc/http/OpenApiServer.java @@ -93,7 +93,8 @@ record HandlerConfig( this.httpServer = HttpServer.create(socketAddress, 0); httpServer.setExecutor(newThreadPerTaskExecutor(ofVirtual().name("http-", 0).factory())); - HttpContext ctx = httpServer.createContext(Optional.ofNullable(spec.basePath()).orElse("/")); + String basePath = Optional.ofNullable(spec.basePath()).orElse("/"); + HttpContext ctx = httpServer.createContext(basePath); ctx.getFilters().add(new ExceptionFilter(exceptionHandler)); ctx.getFilters().add(new RequestPreparationFilter(spec, router, validator, bodyMappers)); ctx.getFilters() @@ -117,7 +118,9 @@ record HandlerConfig( extraCtx.setHandler(e.getValue()); } - httpServer.createContext("/", Handlers.notFoundHandler()); + if (!"/".equals(basePath)) { + httpServer.createContext("/", Handlers.notFoundHandler()); + } httpServer.start(); this.shutdownTimeoutSeconds = shutdownTimeoutSeconds; diff --git a/src/test/java/com/retailsvc/http/OpenApiServerTest.java b/src/test/java/com/retailsvc/http/OpenApiServerTest.java index 4bfa192..03496b2 100644 --- a/src/test/java/com/retailsvc/http/OpenApiServerTest.java +++ b/src/test/java/com/retailsvc/http/OpenApiServerTest.java @@ -89,6 +89,45 @@ void shouldBindOnlyToLoopbackWhenBindAddressIsLoopback() throws Exception { } } + @Test + void shouldDispatchHandlerOnRootPathWhenServerUrlHasNoPath() throws Exception { + Map raw = + Map.of( + "openapi", "3.1.0", + "info", Map.of("title", "Test API", "version", "1.0"), + "servers", List.of(Map.of("url", "http://127.0.0.1:4444")), + "paths", + Map.of( + "/", + Map.of( + "get", + Map.of( + "operationId", + "root", + "responses", + Map.of("204", Map.of("description", "ok")))))); + Spec spec = Spec.from(raw); + RequestHandler rootHandler = request -> Response.empty(); + try (var server = + OpenApiServer.builder() + .spec(spec) + .handlers(Map.of("root", rootHandler)) + .port(0) + .bindAddress(InetAddress.getLoopbackAddress()) + .build()) { + int port = server.listenPort(); + HttpClient client = + HttpClient.newBuilder() + .executor(newVirtualThreadPerTaskExecutor()) + .version(HTTP_1_1) + .build(); + HttpRequest request = + HttpRequest.newBuilder().uri(URI.create("http://127.0.0.1:" + port + "/")).GET().build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.discarding()); + assertThat(response.statusCode()).isEqualTo(HttpURLConnection.HTTP_NO_CONTENT); + } + } + @Test void shouldBindToWildcardWhenBindAddressIsUnset() throws IOException { try (var server =