From e85a004c60c09c1e4780e440187b48728db3bb87 Mon Sep 17 00:00:00 2001 From: Thomas Segismont Date: Fri, 20 Mar 2026 16:49:32 +0100 Subject: [PATCH] Fix missing Accept/Allow headers in 415/405 responses with sub-routers When using sub-routers, the Accept header was empty in 415 (Unsupported Media Type) responses and the Allow header was empty in 405 (Method Not Allowed) responses. This occurred because RoutingContextWrapper accumulated allowed content types and methods but didn't propagate them to the inner context when delegating. Added addAllowedMethods() and addAllowedContentTypes() methods to RoutingContextInternal to properly synchronize this state between wrapper and inner contexts. Some portions of this content were created with the assistance of Claude Code. Signed-off-by: Thomas Segismont --- .../ext/web/impl/RoutingContextDecorator.java | 10 +++ .../ext/web/impl/RoutingContextImplBase.java | 12 ++++ .../ext/web/impl/RoutingContextInternal.java | 16 +++++ .../ext/web/impl/RoutingContextWrapper.java | 4 +- .../io/vertx/ext/web/tests/SubRouterTest.java | 63 +++++++++++++++++++ 5 files changed, 104 insertions(+), 1 deletion(-) diff --git a/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextDecorator.java b/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextDecorator.java index ddb48894ba..1ca7d5da5a 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextDecorator.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextDecorator.java @@ -50,6 +50,16 @@ public RoutingContextInternal setMatchFailure(int matchFailure) { return decoratedContext.setMatchFailure(matchFailure); } + @Override + public RoutingContextInternal addAllowedMethods(java.util.Set allowedMethods) { + return decoratedContext.addAllowedMethods(allowedMethods); + } + + @Override + public RoutingContextInternal addAllowedContentTypes(java.util.Set allowedContentTypes) { + return decoratedContext.addAllowedContentTypes(allowedContentTypes); + } + @Override public int addBodyEndHandler(Handler handler) { return decoratedContext.addBodyEndHandler(handler); diff --git a/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextImplBase.java b/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextImplBase.java index c969eb880f..af4d411b46 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextImplBase.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextImplBase.java @@ -117,6 +117,18 @@ public synchronized RoutingContextInternal setMatchFailure(int matchFailure) { return this; } + @Override + public synchronized RoutingContextInternal addAllowedMethods(Set allowedMethods) { + this.allowedMethods.addAll(allowedMethods); + return this; + } + + @Override + public synchronized RoutingContextInternal addAllowedContentTypes(Set allowedContentTypes) { + this.allowedContentTypes.addAll(allowedContentTypes); + return this; + } + @Override public String mountPoint() { return mountPoint; diff --git a/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextInternal.java b/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextInternal.java index 7ae304facb..b117485ee0 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextInternal.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextInternal.java @@ -58,6 +58,22 @@ public interface RoutingContextInternal extends RoutingContext { */ RoutingContextInternal setMatchFailure(int matchFailure); + /** + * adds allowed methods to the context for 405 responses. + * + * @param allowedMethods the allowed methods to add + * @return fluent self + */ + RoutingContextInternal addAllowedMethods(java.util.Set allowedMethods); + + /** + * adds allowed content types to the context for 415 responses. + * + * @param allowedContentTypes the allowed content types to add + * @return fluent self + */ + RoutingContextInternal addAllowedContentTypes(java.util.Set allowedContentTypes); + /** * @return the current router this context is being routed through. All routingContext is associated with a router and * never returns null. diff --git a/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextWrapper.java b/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextWrapper.java index 8df0b34781..a6e3330cc3 100644 --- a/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextWrapper.java +++ b/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextWrapper.java @@ -186,8 +186,10 @@ public UserContext userContext() { public void next() { if (!super.iterateNext()) { // We didn't route request to anything so go to parent, - // but also propagate the current status + // but also propagate the current status and accumulated allowed methods/content types inner.setMatchFailure(matchFailure); + inner.addAllowedMethods(allowedMethods); + inner.addAllowedContentTypes(allowedContentTypes); inner.next(); } } diff --git a/vertx-web/src/test/java/io/vertx/ext/web/tests/SubRouterTest.java b/vertx-web/src/test/java/io/vertx/ext/web/tests/SubRouterTest.java index 52665bee20..6d344e7d6e 100644 --- a/vertx-web/src/test/java/io/vertx/ext/web/tests/SubRouterTest.java +++ b/vertx-web/src/test/java/io/vertx/ext/web/tests/SubRouterTest.java @@ -784,4 +784,67 @@ private void assertRouterErrorHandlers(String name, Router router, HttpResponseS router.errorHandler(status.code(), ctx -> ctx.response().setStatusCode(status.code()).end(handlerKey)); testRequest(HttpMethod.GET, path, status.code(), status.reasonPhrase(), handlerKey); } + + @Test + public void testSubRouterConsumes() throws Exception { + Router subRouter = Router.router(vertx); + + router.route("/api*").subRouter(subRouter); + + subRouter.route("/resource").consumes("application/json").handler(rc -> rc.response().end("OK")); + + // Test successful content type match + testRequestWithContentType(HttpMethod.POST, "/api/resource", "application/json", 200, "OK"); + + // Test 415 response - Accept header should contain allowed content type from sub-router + testRequestWithContentType(HttpMethod.POST, "/api/resource", "text/xml", 415, "Unsupported Media Type", + res -> assertEquals("application/json", res.getHeader("Accept"))); + } + + @Test + public void testSubRouterConsumesMultiple() throws Exception { + Router subRouter = Router.router(vertx); + + router.route("/api*").subRouter(subRouter); + + subRouter.route("/resource") + .consumes("application/json") + .consumes("text/html; charset=utf-8") + .handler(rc -> rc.response().end("OK")); + + // Test successful content type match + testRequestWithContentType(HttpMethod.POST, "/api/resource", "application/json", 200, "OK"); + testRequestWithContentType(HttpMethod.POST, "/api/resource", "text/html; charset=utf-8", 200, "OK"); + + // Test 415 response - Accept header should contain both allowed content types from sub-router + testRequestWithContentType(HttpMethod.POST, "/api/resource", "text/xml", 415, "Unsupported Media Type", + res -> { + String acceptHeader = res.getHeader("Accept"); + assertNotNull(acceptHeader); + assertTrue(acceptHeader.contains("application/json")); + assertTrue(acceptHeader.contains("text/html; charset=utf-8")); + }); + } + + @Test + public void testSubRouterMethodNotAllowed() throws Exception { + Router subRouter = Router.router(vertx); + + router.route("/api*").subRouter(subRouter); + + subRouter.post("/resource").handler(rc -> rc.response().end("OK")); + subRouter.put("/resource").handler(rc -> rc.response().end("OK")); + + // Test successful method match + testRequest(HttpMethod.POST, "/api/resource", 200, "OK"); + testRequest(HttpMethod.PUT, "/api/resource", 200, "OK"); + + // Test 405 response - Allow header should contain allowed methods from sub-router + testRequest(HttpMethod.GET, "/api/resource", null, res -> { + String allowHeader = res.getHeader("Allow"); + assertNotNull(allowHeader); + assertTrue(allowHeader.contains("POST")); + assertTrue(allowHeader.contains("PUT")); + }, 405, "Method Not Allowed", null); + } }