Skip to content

Commit 819e6b3

Browse files
committed
fix: Enforce exact path match on extra routes
JDK HttpServer.createContext does prefix matching, so /health and /alive were dispatched for /health232 or /alive/34. ExtraRouteAdapter now rejects any request whose path does not exactly equal the registered route.
1 parent 897d079 commit 819e6b3

4 files changed

Lines changed: 110 additions & 3 deletions

File tree

src/main/java/com/retailsvc/http/OpenApiServer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ record HandlerConfig(
137137
for (Map.Entry<String, RequestHandler> e : handlerConfig.extras().entrySet()) {
138138
HttpContext extraCtx = httpServer.createContext(e.getKey());
139139
extraCtx.getFilters().add(new ExceptionFilter(exceptionHandler, renderer));
140-
extraCtx.setHandler(new ExtraRouteAdapter(e.getValue(), renderer));
140+
extraCtx.setHandler(new ExtraRouteAdapter(e.getKey(), e.getValue(), renderer));
141141
}
142142

143143
if (!"/".equals(basePath)) {

src/main/java/com/retailsvc/http/internal/ExtraRouteAdapter.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.retailsvc.http.internal;
22

3+
import com.retailsvc.http.NotFoundException;
34
import com.retailsvc.http.Request;
45
import com.retailsvc.http.RequestHandler;
56
import com.retailsvc.http.Response;
@@ -19,16 +20,22 @@
1920
*/
2021
public final class ExtraRouteAdapter implements HttpHandler {
2122

23+
private final String path;
2224
private final RequestHandler handler;
2325
private final ResponseRenderer renderer;
2426

25-
public ExtraRouteAdapter(RequestHandler handler, ResponseRenderer renderer) {
27+
public ExtraRouteAdapter(String path, RequestHandler handler, ResponseRenderer renderer) {
28+
this.path = path;
2629
this.handler = handler;
2730
this.renderer = renderer;
2831
}
2932

3033
@Override
3134
public void handle(HttpExchange exchange) throws IOException {
35+
String requested = exchange.getRequestURI().getPath();
36+
if (!path.equals(requested)) {
37+
throw new NotFoundException(exchange.getRequestMethod() + " " + requested);
38+
}
3239
byte[] body = exchange.getRequestBody().readAllBytes();
3340
HttpMethod method = HttpMethod.parse(exchange.getRequestMethod());
3441
var headers = exchange.getRequestHeaders();
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package com.retailsvc.http;
2+
3+
import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
4+
import static java.net.HttpURLConnection.HTTP_OK;
5+
import static org.assertj.core.api.Assertions.assertThat;
6+
7+
import java.net.URI;
8+
import java.net.http.HttpClient;
9+
import java.net.http.HttpRequest;
10+
import java.net.http.HttpResponse;
11+
import java.net.http.HttpResponse.BodyHandlers;
12+
import java.util.Map;
13+
import org.junit.jupiter.api.Test;
14+
15+
class ExactUrlMatchingIT extends ServerBaseTest {
16+
17+
@Test
18+
void extraRouteRejectsTrailingSuffix() throws Exception {
19+
try (var s =
20+
newBuilder()
21+
.spec(spec)
22+
.handlers(stubAllHandlers(Map.of()))
23+
.port(0)
24+
.extraRoute("/alive", Handlers.aliveHandler())
25+
.build();
26+
var client = httpClient()) {
27+
28+
HttpResponse<String> resp = get(client, s, "/alive232");
29+
30+
assertThat(resp.statusCode()).isEqualTo(HTTP_NOT_FOUND);
31+
}
32+
}
33+
34+
@Test
35+
void extraRouteRejectsSubPath() throws Exception {
36+
try (var s =
37+
newBuilder()
38+
.spec(spec)
39+
.handlers(stubAllHandlers(Map.of()))
40+
.port(0)
41+
.extraRoute("/alive", Handlers.aliveHandler())
42+
.build();
43+
var client = httpClient()) {
44+
45+
HttpResponse<String> resp = get(client, s, "/alive/34");
46+
47+
assertThat(resp.statusCode()).isEqualTo(HTTP_NOT_FOUND);
48+
}
49+
}
50+
51+
@Test
52+
void extraRouteAcceptsExactPath() throws Exception {
53+
try (var s =
54+
newBuilder()
55+
.spec(spec)
56+
.handlers(stubAllHandlers(Map.of()))
57+
.port(0)
58+
.extraRoute("/alive", Handlers.aliveHandler())
59+
.build();
60+
var client = httpClient()) {
61+
62+
HttpResponse<String> resp = get(client, s, "/alive");
63+
64+
assertThat(resp.statusCode()).isEqualTo(204);
65+
}
66+
}
67+
68+
@Test
69+
void specRouteRejectsTrailingSuffix() throws Exception {
70+
Map<String, RequestHandler> handlers = Map.of("get-data", req -> Response.status(HTTP_OK));
71+
try (var s = newBuilder().spec(spec).handlers(stubAllHandlers(handlers)).port(0).build();
72+
var client = httpClient()) {
73+
74+
HttpResponse<String> resp = get(client, s, "/api/v1/data232");
75+
76+
assertThat(resp.statusCode()).isEqualTo(HTTP_NOT_FOUND);
77+
}
78+
}
79+
80+
@Test
81+
void specRouteRejectsBasePathSuffix() throws Exception {
82+
try (var s = newBuilder().spec(spec).handlers(stubAllHandlers(Map.of())).port(0).build();
83+
var client = httpClient()) {
84+
85+
HttpResponse<String> resp = get(client, s, "/api/v1xyz/data");
86+
87+
assertThat(resp.statusCode()).isEqualTo(HTTP_NOT_FOUND);
88+
}
89+
}
90+
91+
private HttpResponse<String> get(HttpClient client, OpenApiServer s, String path)
92+
throws Exception {
93+
var req =
94+
HttpRequest.newBuilder()
95+
.uri(URI.create("http://localhost:" + s.listenPort() + path))
96+
.GET()
97+
.build();
98+
return client.send(req, BodyHandlers.ofString());
99+
}
100+
}

src/test/java/com/retailsvc/http/internal/ExtraRouteAdapterTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ void buildsRequestWithMethodQueryHeadersAndBodyBytesAndNullOperationId() throws
3232

3333
Map<String, TypeMapper> mappers = Map.of("application/json", new GsonTypeMapper());
3434
ResponseRenderer renderer = new ResponseRenderer(mappers);
35-
ExtraRouteAdapter adapter = new ExtraRouteAdapter(handler, renderer);
35+
ExtraRouteAdapter adapter = new ExtraRouteAdapter("/alive", handler, renderer);
3636

3737
HttpExchange ex = mock(HttpExchange.class);
3838
Headers reqHeaders = new Headers();

0 commit comments

Comments
 (0)