Repository: vert-x3/vertx-web
Issue: "405 Method Not Allowed" response not adding allow header

As per this document, https://vertx.io/docs/vertx-web/java/#_route_match_failures Vert.x-Web will signal a 405 error If a route matches the path but doesn’t match the HTTP Method, but as per Mozilla document https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405 response should contain `allow` header. Is there any reason why it's not added in response?

It only adds `content-length` header

Version - 4.3.4

Changed files:
vertx-web/src/test/java/io/vertx/ext/web/RouterTest.java, vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextImplBase.java, vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextImpl.java

diff --git a/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextImpl.java b/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextImpl.java
index c6959902b..07da73ab5 100644
--- a/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextImpl.java
+++ b/vertx-web/src/main/java/io/vertx/ext/web/impl/RoutingContextImpl.java
@@ -35,6 +35,7 @@ import io.vertx.ext.web.handler.impl.UserHolder;
 import java.nio.charset.Charset;
 import java.util.*;
 import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
+import java.util.stream.Collectors;
 
 import static io.vertx.ext.web.handler.impl.SessionHandlerImpl.SESSION_USER_HOLDER_KEY;
 
@@ -158,6 +159,10 @@ public class RoutingContextImpl extends RoutingContextImplBase {
           this.response()
             .putHeader(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=utf-8")
             .end(DEFAULT_404);
+        } else if (this.request().method() != HttpMethod.HEAD && matchFailure == 405) {
+          // If it's a 405 let's send a body too
+          this.response()
+            .putHeader(HttpHeaderNames.ALLOW, allowedMethods.stream().map(HttpMethod::name).collect(Collectors.joining(","))).end();
         } else {
           this.response().end();
         }
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 58ff9d1f5..1e9860d95 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
@@ -18,6 +18,7 @@ package io.vertx.ext.web.impl;
 
 import io.netty.handler.codec.http.HttpResponseStatus;
 import io.vertx.core.Handler;
+import io.vertx.core.http.HttpMethod;
 import io.vertx.core.impl.logging.Logger;
 import io.vertx.core.impl.logging.LoggerFactory;
 import io.vertx.ext.web.Route;
@@ -25,6 +26,7 @@ import io.vertx.ext.web.Router;
 import io.vertx.ext.web.RoutingContext;
 import io.vertx.ext.web.handler.HttpException;
 
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
@@ -58,6 +60,8 @@ public abstract class RoutingContextImplBase implements RoutingContextInternal {
   // internal runtime state
   private volatile long seen;
 
+  protected Set<HttpMethod> allowedMethods = new HashSet<>();
+
   RoutingContextImplBase(String mountPoint, Set<RouteImpl> routes, Router currentRouter) {
     this.mountPoint = mountPoint;
     this.routes = routes;
@@ -179,6 +183,8 @@ public abstract class RoutingContextImplBase implements RoutingContextInternal {
           }
           return true;
         } else if (matchResult == 405) {
+          //We need to add supported methods for route in case we need to send 405 at end
+          allowedMethods.addAll(routeState.getMethods());
           // invalid method match, means that
           // we should "update" the failure if not found to be invalid method
           if (this.matchFailure == 404) {
diff --git a/vertx-web/src/test/java/io/vertx/ext/web/RouterTest.java b/vertx-web/src/test/java/io/vertx/ext/web/RouterTest.java
index d8eaa9b8d..fe0168e7d 100644
--- a/vertx-web/src/test/java/io/vertx/ext/web/RouterTest.java
+++ b/vertx-web/src/test/java/io/vertx/ext/web/RouterTest.java
@@ -2761,6 +2761,18 @@ public class RouterTest extends WebTestBase {
     testRequest(HttpMethod.PUT, "/path", HttpResponseStatus.METHOD_NOT_ALLOWED);
   }
 
+  @Test
+  public void testMethodNotAllowedHeader() throws Exception {
+    router.get("/path").handler(rc -> rc.response().end());
+    router.post("/path").handler(rc -> rc.response().end());
+    router.put("/hello").handler(rc -> rc.response().end());
+    testRequest(HttpMethod.PUT, "/path", null, res -> {
+      assertEquals(2, res.getHeader("allow").split(",").length);
+      assertTrue(res.getHeader("allow").contains("GET"));
+      assertTrue(res.getHeader("allow").contains("POST"));
+    }, HttpResponseStatus.METHOD_NOT_ALLOWED.code(), HttpResponseStatus.METHOD_NOT_ALLOWED.reasonPhrase(), null);
+  }
+
   @Test
   public void testNotAcceptableStatusCode() throws Exception {
     router.route().produces("text/html").handler(rc -> rc.response().end());