Repository: eclipse-vertx/vert.x

Problem statement:
HttpMethod is an interface
In Vert.x 2 and 3, the `HttpMethod` is declared as an enum. This leashes the extensibility of HTTP and prevents modelling other HTTP methods with this type unless using `HttpMethod#OTHER` in addition with the `rawMethod` attribute on `HttpServerRequest` and `HttpClientRequest`.

We can turn this enum into an interface in Vert.x 4 with a few benefits:

1. non predefined HTTP methods are now handled like others without special handling
2. one can define its own HTTP method constant for other protocols (like 

And at the expanse of 2 breakages:

1. the enum cannot be used as is dispatching a call with a `switch` construct. Instead the switch must be done on the `HttpMethod#name()` string value.
2. the `rawMethod` property is removed because now the `HttpMethod` can model the raw value.




Hints:

Patch:
diff --git a/src/main/java/io/vertx/core/http/HttpClientRequest.java b/src/main/java/io/vertx/core/http/HttpClientRequest.java
--- a/src/main/java/io/vertx/core/http/HttpClientRequest.java
+++ b/src/main/java/io/vertx/core/http/HttpClientRequest.java
@@ -99,20 +99,6 @@ public interface HttpClientRequest extends WriteStream<Buffer>, Future<HttpClien
    */
   HttpMethod method();
 
-  /**
-   * @return the raw value of the method this request sends
-   */
-  String getRawMethod();
-
-  /**
-   * Set the value the method to send when the method {@link HttpMethod#OTHER} is used.
-   *
-   * @param method the raw method
-   * @return a reference to this, so the API can be used fluently
-   */
-  @Fluent
-  HttpClientRequest setRawMethod(String method);
-
   /**
    * @return the absolute URI corresponding to the the HTTP request
    */
diff --git a/src/main/java/io/vertx/core/http/HttpMethod.java b/src/main/java/io/vertx/core/http/HttpMethod.java
--- a/src/main/java/io/vertx/core/http/HttpMethod.java
+++ b/src/main/java/io/vertx/core/http/HttpMethod.java
@@ -12,13 +12,109 @@
 package io.vertx.core.http;
 
 import io.vertx.codegen.annotations.VertxGen;
+import io.vertx.core.http.impl.HttpMethodImpl;
+
+import java.util.Objects;
 
 /**
- * Represents an HTTP method
+ * Represents an HTTP method.
  *
  * @author <a href="http://tfox.org">Tim Fox</a>
  */
 @VertxGen
-public enum HttpMethod {
-  OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT, PATCH, OTHER
+public interface HttpMethod {
+
+  /**
+   * The {@code OPTIONS} method, this instance is interned and uniquely used.
+   */
+  HttpMethod OPTIONS = new HttpMethodImpl(io.netty.handler.codec.http.HttpMethod.OPTIONS);
+
+  /**
+   * The {@code GET} method, this instance is interned and uniquely used.
+   */
+  HttpMethod GET = new HttpMethodImpl(io.netty.handler.codec.http.HttpMethod.GET);
+
+  /**
+   * The {@code HEAD} method, this instance is interned and uniquely used.
+   */
+  HttpMethod HEAD = new HttpMethodImpl(io.netty.handler.codec.http.HttpMethod.HEAD);
+
+  /**
+   * The {@code POST} method, this instance is interned and uniquely used.
+   */
+  HttpMethod POST = new HttpMethodImpl(io.netty.handler.codec.http.HttpMethod.POST);
+
+  /**
+   * The {@code PUT} method, this instance is interned and uniquely used.
+   */
+  HttpMethod PUT = new HttpMethodImpl(io.netty.handler.codec.http.HttpMethod.PUT);
+
+  /**
+   * The {@code DELETE} method, this instance is interned and uniquely used.
+   */
+  HttpMethod DELETE = new HttpMethodImpl(io.netty.handler.codec.http.HttpMethod.DELETE);
+
+  /**
+   * The {@code TRACE} method, this instance is interned and uniquely used.
+   */
+  HttpMethod TRACE = new HttpMethodImpl(io.netty.handler.codec.http.HttpMethod.TRACE);
+
+  /**
+   * The {@code CONNECT} method, this instance is interned and uniquely used.
+   */
+  HttpMethod CONNECT = new HttpMethodImpl(io.netty.handler.codec.http.HttpMethod.CONNECT);
+
+  /**
+   * The {@code PATCH} method, this instance is interned and uniquely used.
+   */
+  HttpMethod PATCH = new HttpMethodImpl(io.netty.handler.codec.http.HttpMethod.PATCH);
+
+  /**
+   * @return the method name
+   */
+  String name();
+
+  /**
+   * @return the same value than {@link #name()}
+   */
+  String toString();
+
+  /**
+   * Lookup the {@code HttpMethod} value for the specified {@code value}.
+   * <br/>
+   * The predefined method constants {@link #GET}, {@link #POST}, {@link #PUT}, {@link #HEAD}, {@link #OPTIONS},
+   * {@link #DELETE}, {@link #TRACE}, {@link #CONNECT} and {@link #PATCH} are interned and will be returned
+   * when case sensitively matching their string value (i.e {@code "GET"}, etc...)
+   * <br/>
+   * Otherwise a new instance is returned.
+   *
+   * @param value the value
+   * @return the {@code HttpMethod} instance for the specified string {@code value}
+   * @throws IllegalArgumentException when the value is incorrect, the value is empty or contains an invalid char
+   */
+  static HttpMethod valueOf(String value) {
+    Objects.requireNonNull(value, "value");
+    switch (value) {
+      case "OPTIONS":
+        return OPTIONS;
+      case "GET":
+        return GET;
+      case "HEAD":
+        return HEAD;
+      case "POST":
+        return POST;
+      case "PUT":
+        return PUT;
+      case "DELETE":
+        return DELETE;
+      case "TRACE":
+        return TRACE;
+      case "CONNECT":
+        return CONNECT;
+      case "PATCH":
+        return PATCH;
+      default:
+        return new HttpMethodImpl(io.netty.handler.codec.http.HttpMethod.valueOf(value));
+    }
+  }
 }
diff --git a/src/main/java/io/vertx/core/http/HttpServerRequest.java b/src/main/java/io/vertx/core/http/HttpServerRequest.java
--- a/src/main/java/io/vertx/core/http/HttpServerRequest.java
+++ b/src/main/java/io/vertx/core/http/HttpServerRequest.java
@@ -72,11 +72,6 @@ public interface HttpServerRequest extends ReadStream<Buffer> {
    */
   HttpMethod method();
 
-  /**
-   * @return the HTTP method as sent by the client
-   */
-  String rawMethod();
-
   /**
    * @return true if this {@link io.vertx.core.net.NetSocket} is encrypted via SSL/TLS
    */
diff --git a/src/main/java/io/vertx/core/http/impl/Http1xClientConnection.java b/src/main/java/io/vertx/core/http/impl/Http1xClientConnection.java
--- a/src/main/java/io/vertx/core/http/impl/Http1xClientConnection.java
+++ b/src/main/java/io/vertx/core/http/impl/Http1xClientConnection.java
@@ -117,12 +117,11 @@ ConnectionListener<HttpClientConnection> listener() {
 
   private HttpRequest createRequest(
     HttpMethod method,
-    String rawMethod,
     String uri,
     MultiMap headerMap,
     String hostHeader,
     boolean chunked) {
-    DefaultHttpRequest request = new DefaultHttpRequest(HttpUtils.toNettyHttpVersion(version), HttpUtils.toNettyHttpMethod(method, rawMethod), uri, false);
+    DefaultHttpRequest request = new DefaultHttpRequest(HttpUtils.toNettyHttpVersion(version), HttpMethodImpl.toNetty(method), uri, false);
     HttpHeaders headers = request.headers();
     if (headerMap != null) {
       for (Map.Entry<String, String> header : headerMap) {
@@ -313,8 +312,8 @@ public ContextInternal getContext() {
     }
 
     @Override
-    public void writeHead(HttpMethod method, String rawMethod, String uri, MultiMap headers, String hostHeader, boolean chunked, ByteBuf buf, boolean end, StreamPriority priority, Handler<AsyncResult<Void>> handler) {
-      HttpRequest request = conn.createRequest(method, rawMethod, uri, headers, hostHeader, chunked);
+    public void writeHead(HttpMethod method, String uri, MultiMap headers, String hostHeader, boolean chunked, ByteBuf buf, boolean end, StreamPriority priority, Handler<AsyncResult<Void>> handler) {
+      HttpRequest request = conn.createRequest(method, uri, headers, hostHeader, chunked);
       if (end) {
         if (buf != null) {
           request = new AssembledFullHttpRequest(request, buf);
diff --git a/src/main/java/io/vertx/core/http/impl/Http1xServerRequest.java b/src/main/java/io/vertx/core/http/impl/Http1xServerRequest.java
--- a/src/main/java/io/vertx/core/http/impl/Http1xServerRequest.java
+++ b/src/main/java/io/vertx/core/http/impl/Http1xServerRequest.java
@@ -65,7 +65,6 @@ public class Http1xServerRequest implements HttpServerRequest {
   private HttpRequest request;
   private io.vertx.core.http.HttpVersion version;
   private io.vertx.core.http.HttpMethod method;
-  private String rawMethod;
   private String uri;
   private String path;
   private String query;
@@ -208,24 +207,11 @@ public io.vertx.core.http.HttpVersion version() {
   @Override
   public io.vertx.core.http.HttpMethod method() {
     if (method == null) {
-      String sMethod = request.method().toString();
-      try {
-        method = io.vertx.core.http.HttpMethod.valueOf(sMethod);
-      } catch (IllegalArgumentException e) {
-        method = io.vertx.core.http.HttpMethod.OTHER;
-      }
+      method = io.vertx.core.http.impl.HttpMethodImpl.fromNetty(request.method());
     }
     return method;
   }
 
-  @Override
-  public String rawMethod() {
-    if (rawMethod == null) {
-      rawMethod = request.method().toString();
-    }
-    return rawMethod;
-  }
-
   @Override
   public String uri() {
     if (uri == null) {
diff --git a/src/main/java/io/vertx/core/http/impl/Http2ClientConnection.java b/src/main/java/io/vertx/core/http/impl/Http2ClientConnection.java
--- a/src/main/java/io/vertx/core/http/impl/Http2ClientConnection.java
+++ b/src/main/java/io/vertx/core/http/impl/Http2ClientConnection.java
@@ -17,7 +17,6 @@
 import io.netty.handler.codec.http.HttpHeaderNames;
 import io.netty.handler.codec.http.HttpResponseStatus;
 import io.netty.handler.codec.http2.DefaultHttp2Headers;
-import io.netty.handler.codec.http2.Http2Connection;
 import io.netty.handler.codec.http2.Http2Error;
 import io.netty.handler.codec.http2.Http2Exception;
 import io.netty.handler.codec.http2.Http2Headers;
@@ -166,7 +165,7 @@ public synchronized void onPushPromiseRead(ChannelHandlerContext ctx, int stream
       Handler<HttpClientRequest> pushHandler = stream.pushHandler();
       if (pushHandler != null) {
         String rawMethod = headers.method().toString();
-        HttpMethod method = HttpUtils.toVertxMethod(rawMethod);
+        HttpMethod method = HttpMethod.valueOf(rawMethod);
         String uri = headers.path().toString();
         String authority = headers.authority() != null ? headers.authority().toString() : null;
         MultiMap headersMap = new Http2HeadersAdaptor(headers);
@@ -181,7 +180,7 @@ public synchronized void onPushPromiseRead(ChannelHandlerContext ctx, int stream
           host = authority.substring(0, pos);
           port = Integer.parseInt(authority.substring(pos + 1));
         }
-        HttpClientRequestPushPromise pushReq = new HttpClientRequestPushPromise(this, client, isSsl(), method, rawMethod, uri, host, port, headersMap);
+        HttpClientRequestPushPromise pushReq = new HttpClientRequestPushPromise(this, client, isSsl(), method, uri, host, port, headersMap);
         pushReq.getStream().init(promisedStream);
         if (metrics != null) {
           ((Stream)pushReq.getStream()).metric = metrics.responsePushed(queueMetric, metric(), localAddress(), remoteAddress(), pushReq);
@@ -455,9 +454,9 @@ Handler<HttpClientRequest> pushHandler() {
     }
 
     @Override
-    public void writeHead(HttpMethod method, String rawMethod, String uri, MultiMap headers, String hostHeader, boolean chunked, ByteBuf content, boolean end, StreamPriority priority, Handler<AsyncResult<Void>> handler) {
+    public void writeHead(HttpMethod method, String uri, MultiMap headers, String hostHeader, boolean chunked, ByteBuf content, boolean end, StreamPriority priority, Handler<AsyncResult<Void>> handler) {
       Http2Headers h = new DefaultHttp2Headers();
-      h.method(method != HttpMethod.OTHER ? method.name() : rawMethod);
+      h.method(method.name());
       boolean e;
       if (method == HttpMethod.CONNECT) {
         if (hostHeader == null) {
diff --git a/src/main/java/io/vertx/core/http/impl/Http2ServerConnection.java b/src/main/java/io/vertx/core/http/impl/Http2ServerConnection.java
--- a/src/main/java/io/vertx/core/http/impl/Http2ServerConnection.java
+++ b/src/main/java/io/vertx/core/http/impl/Http2ServerConnection.java
@@ -132,11 +132,7 @@ void sendPush(int streamId, String host, HttpMethod method, MultiMap headers, St
 
   private synchronized void doSendPush(int streamId, String host, HttpMethod method, MultiMap headers, String path, StreamPriority streamPriority, Promise<HttpServerResponse> promise) {
     Http2Headers headers_ = new DefaultHttp2Headers();
-    if (method == HttpMethod.OTHER) {
-      throw new IllegalArgumentException("Cannot push HttpMethod.OTHER");
-    } else {
-      headers_.method(method.name());
-    }
+    headers_.method(method.name());
     headers_.path(path);
     headers_.scheme(isSsl() ? "https" : "http");
     if (host != null) {
diff --git a/src/main/java/io/vertx/core/http/impl/Http2ServerRequestImpl.java b/src/main/java/io/vertx/core/http/impl/Http2ServerRequestImpl.java
--- a/src/main/java/io/vertx/core/http/impl/Http2ServerRequestImpl.java
+++ b/src/main/java/io/vertx/core/http/impl/Http2ServerRequestImpl.java
@@ -419,7 +419,7 @@ public HttpServerRequest setExpectMultipart(boolean expect) {
         if (postRequestDecoder == null) {
           String contentType = headersMap.get(HttpHeaderNames.CONTENT_TYPE);
           if (contentType != null) {
-            io.netty.handler.codec.http.HttpMethod method = io.netty.handler.codec.http.HttpMethod.valueOf(rawMethod);
+            io.netty.handler.codec.http.HttpMethod method = HttpMethodImpl.toNetty(this.method);
             String lowerCaseContentType = contentType.toString().toLowerCase();
             boolean isURLEncoded = lowerCaseContentType.startsWith(HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED.toString());
             if ((lowerCaseContentType.startsWith(HttpHeaderValues.MULTIPART_FORM_DATA.toString()) || isURLEncoded) &&
diff --git a/src/main/java/io/vertx/core/http/impl/Http2ServerStream.java b/src/main/java/io/vertx/core/http/impl/Http2ServerStream.java
--- a/src/main/java/io/vertx/core/http/impl/Http2ServerStream.java
+++ b/src/main/java/io/vertx/core/http/impl/Http2ServerStream.java
@@ -27,7 +27,6 @@
 abstract class Http2ServerStream extends VertxHttp2Stream<Http2ServerConnection> {
 
   protected final Http2Headers headers;
-  protected final String rawMethod;
   protected final HttpMethod method;
   protected final String uri;
   protected final String host;
@@ -43,7 +42,6 @@ abstract class Http2ServerStream extends VertxHttp2Stream<Http2ServerConnection>
 
     this.headers = null;
     this.method = method;
-    this.rawMethod = method.name();
     this.uri = uri;
     this.host = null;
     this.response = new Http2ServerResponseImpl(conn, this, true, contentEncoding, null);
@@ -61,8 +59,7 @@ abstract class Http2ServerStream extends VertxHttp2Stream<Http2ServerConnection>
     this.headers = headers;
     this.host = host;
     this.uri = headers.get(":path") != null ? headers.get(":path").toString() : null;
-    this.rawMethod = headers.get(":method") != null ? headers.get(":method").toString() : null;
-    this.method = HttpUtils.toVertxMethod(rawMethod);
+    this.method = headers.get(":method") != null ? HttpMethod.valueOf(headers.get(":method").toString()) : null;
     this.response = new Http2ServerResponseImpl(conn, this, false, contentEncoding, host);
   }
 
@@ -115,10 +112,6 @@ public HttpMethod method() {
     return method;
   }
 
-  public String rawMethod() {
-    return rawMethod;
-  }
-
   @Override
   void handleClose() {
     super.handleClose();
diff --git a/src/main/java/io/vertx/core/http/impl/Http2UpgradedClientConnection.java b/src/main/java/io/vertx/core/http/impl/Http2UpgradedClientConnection.java
--- a/src/main/java/io/vertx/core/http/impl/Http2UpgradedClientConnection.java
+++ b/src/main/java/io/vertx/core/http/impl/Http2UpgradedClientConnection.java
@@ -103,7 +103,6 @@ public HttpClientConnection connection() {
      */
     @Override
     public void writeHead(HttpMethod method,
-                          String rawMethod,
                           String uri,
                           MultiMap headers,
                           String hostHeader,
@@ -171,7 +170,7 @@ public void upgradeTo(ChannelHandlerContext ctx, FullHttpResponse upgradeRespons
       HttpClientUpgradeHandler upgradeHandler = new HttpClientUpgradeHandler(httpCodec, upgradeCodec, 65536);
       pipeline.addAfter("codec", null, new UpgradeRequestHandler());
       pipeline.addAfter("codec", null, upgradeHandler);
-      stream.writeHead(method, rawMethod, uri, headers, hostHeader, chunked, buf, end, priority, listener);
+      stream.writeHead(method, uri, headers, hostHeader, chunked, buf, end, priority, listener);
     }
 
     @Override
diff --git a/src/main/java/io/vertx/core/http/impl/HttpClientRequestImpl.java b/src/main/java/io/vertx/core/http/impl/HttpClientRequestImpl.java
--- a/src/main/java/io/vertx/core/http/impl/HttpClientRequestImpl.java
+++ b/src/main/java/io/vertx/core/http/impl/HttpClientRequestImpl.java
@@ -14,7 +14,6 @@
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.CompositeByteBuf;
 import io.netty.buffer.Unpooled;
-import io.vertx.codegen.annotations.Nullable;
 import io.vertx.core.*;
 import io.vertx.core.buffer.Buffer;
 import io.vertx.core.http.HttpClientRequest;
@@ -57,7 +56,6 @@ public class HttpClientRequestImpl extends HttpClientRequestBase implements Http
   private final Future<Void> endFuture;
   private boolean chunked;
   private String hostHeader;
-  private String rawMethod;
   private Handler<Void> continueHandler;
   private Handler<Void> drainHandler;
   private Handler<HttpClientRequest> pushHandler;
@@ -151,17 +149,6 @@ public synchronized boolean isChunked() {
     return chunked;
   }
 
-  @Override
-  public synchronized String getRawMethod() {
-    return rawMethod;
-  }
-
-  @Override
-  public synchronized HttpClientRequest setRawMethod(String method) {
-    this.rawMethod = method;
-    return this;
-  }
-
   @Override
   public synchronized HttpClientRequest setHost(String host) {
     this.hostHeader = host;
@@ -403,11 +390,6 @@ protected String hostHeader() {
 
   private synchronized void connect(Handler<AsyncResult<HttpVersion>> headersHandler) {
     if (!connecting) {
-
-      if (method == HttpMethod.OTHER && rawMethod == null) {
-        throw new IllegalStateException("You must provide a rawMethod when using an HttpMethod.OTHER method");
-      }
-
       SocketAddress peerAddress;
       if (hostHeader != null) {
         int idx = hostHeader.lastIndexOf(':');
@@ -478,7 +460,7 @@ private void connected(Handler<AsyncResult<HttpVersion>> headersHandler, HttpCli
           }
         };
       }
-      stream.writeHead(method, rawMethod, uri, headers, hostHeader(), chunked, pending, ended, priority, handler);
+      stream.writeHead(method, uri, headers, hostHeader(), chunked, pending, ended, priority, handler);
       if (ended) {
         tryComplete();
       }
diff --git a/src/main/java/io/vertx/core/http/impl/HttpClientRequestPushPromise.java b/src/main/java/io/vertx/core/http/impl/HttpClientRequestPushPromise.java
--- a/src/main/java/io/vertx/core/http/impl/HttpClientRequestPushPromise.java
+++ b/src/main/java/io/vertx/core/http/impl/HttpClientRequestPushPromise.java
@@ -27,7 +27,6 @@ class HttpClientRequestPushPromise extends HttpClientRequestBase {
 
   private final Http2ClientConnection conn;
   private final Http2ClientConnection.StreamImpl stream;
-  private final String rawMethod;
   private final MultiMap headers;
 
   public HttpClientRequestPushPromise(
@@ -35,7 +34,6 @@ public HttpClientRequestPushPromise(
     HttpClientImpl client,
     boolean ssl,
     HttpMethod method,
-    String rawMethod,
     String uri,
     String host,
     int port,
@@ -43,7 +41,6 @@ public HttpClientRequestPushPromise(
     super(client, conn.getContext(), ssl, method, SocketAddress.inetSocketAddress(port, host), host, port, uri);
     this.conn = conn;
     this.stream = new Http2ClientConnection.StreamImpl(conn, conn.getContext(), this, null);
-    this.rawMethod = rawMethod;
     this.headers = headers;
   }
 
@@ -82,16 +79,6 @@ public HttpMethod method() {
     return method;
   }
 
-  @Override
-  public String getRawMethod() {
-    return rawMethod;
-  }
-
-  @Override
-  public HttpClientRequest setRawMethod(String method) {
-    throw new IllegalStateException();
-  }
-
   @Override
   public String uri() {
     return uri;
diff --git a/src/main/java/io/vertx/core/http/impl/HttpClientStream.java b/src/main/java/io/vertx/core/http/impl/HttpClientStream.java
--- a/src/main/java/io/vertx/core/http/impl/HttpClientStream.java
+++ b/src/main/java/io/vertx/core/http/impl/HttpClientStream.java
@@ -12,17 +12,14 @@
 package io.vertx.core.http.impl;
 
 import io.netty.buffer.ByteBuf;
-import io.netty.util.concurrent.FutureListener;
 import io.vertx.core.AsyncResult;
 import io.vertx.core.Handler;
 import io.vertx.core.MultiMap;
-import io.vertx.core.Promise;
 import io.vertx.core.http.HttpConnection;
 import io.vertx.core.http.HttpMethod;
 import io.vertx.core.http.HttpVersion;
 import io.vertx.core.http.StreamPriority;
 import io.vertx.core.impl.ContextInternal;
-import io.vertx.core.net.NetSocket;
 
 /**
  * @author <a href="mailto:julien@julienviet.com">Julien Viet</a>
@@ -45,7 +42,7 @@ public interface HttpClientStream {
   HttpConnection connection();
   ContextInternal getContext();
 
-  void writeHead(HttpMethod method, String rawMethod, String uri, MultiMap headers, String hostHeader, boolean chunked, ByteBuf buf, boolean end, StreamPriority priority, Handler<AsyncResult<Void>> listener);
+  void writeHead(HttpMethod method, String uri, MultiMap headers, String hostHeader, boolean chunked, ByteBuf buf, boolean end, StreamPriority priority, Handler<AsyncResult<Void>> listener);
   void writeBuffer(ByteBuf buf, boolean end, Handler<AsyncResult<Void>> listener);
   void writeFrame(int type, int flags, ByteBuf payload);
 
diff --git a/src/main/java/io/vertx/core/http/impl/HttpMethodImpl.java b/src/main/java/io/vertx/core/http/impl/HttpMethodImpl.java
new file mode 100644
--- /dev/null
+++ b/src/main/java/io/vertx/core/http/impl/HttpMethodImpl.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (c) 2011-2019 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ * which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+ */
+package io.vertx.core.http.impl;
+
+import io.vertx.core.http.HttpMethod;
+
+import java.util.Objects;
+
+public class HttpMethodImpl implements HttpMethod {
+
+  public static io.netty.handler.codec.http.HttpMethod toNetty(HttpMethod method) {
+    if (method instanceof HttpMethodImpl) {
+      return ((HttpMethodImpl) method).nettyMethod;
+    } else {
+      return io.netty.handler.codec.http.HttpMethod.valueOf(method.name());
+    }
+  }
+
+  /**
+   * Lookup the {@code HttpMethod} value for the specified {@code nettyMethod}.
+   * <br/>
+   * The predefined method constants {@link #GET}, {@link #POST}, {@link #PUT}, {@link #HEAD}, {@link #OPTIONS},
+   * {@link #DELETE}, {@link #TRACE}, {@link #CONNECT} and {@link #PATCH} are interned and will be returned
+   * when case sensitively matching their string value (i.e {@code "GET"}, etc...)
+   * <br/>
+   * Otherwise a new instance is returned.
+   *
+   * @param method the netty method
+   * @return the {@code HttpMethod} instance for the specified netty {@code method}
+   */
+  public static HttpMethod fromNetty(io.netty.handler.codec.http.HttpMethod method) {
+    // Fast path
+    if (method == io.netty.handler.codec.http.HttpMethod.GET) {
+      return GET;
+    } else if (method == io.netty.handler.codec.http.HttpMethod.POST) {
+      return POST;
+    } else {
+      // Keep method small
+      return _fromNetty(method);
+    }
+  }
+
+  private static HttpMethod _fromNetty(io.netty.handler.codec.http.HttpMethod sMethod) {
+    switch (sMethod.name()) {
+      case "OPTIONS":
+        return OPTIONS;
+      case "HEAD":
+        return HEAD;
+      case "PUT":
+        return PUT;
+      case "DELETE":
+        return DELETE;
+      case "TRACE":
+        return TRACE;
+      case "CONNECT":
+        return CONNECT;
+      case "PATCH":
+        return PATCH;
+      default:
+        return new HttpMethodImpl(sMethod);
+    }
+  }
+
+  private final io.netty.handler.codec.http.HttpMethod nettyMethod;
+
+  public HttpMethodImpl(io.netty.handler.codec.http.HttpMethod nettyMethod) {
+    this.nettyMethod = nettyMethod;
+  }
+
+  @Override
+  public String name() {
+    return nettyMethod.name();
+  }
+
+  @Override
+  public int hashCode() {
+    return nettyMethod.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj == this) {
+      return true;
+    }
+    if (obj instanceof HttpMethod) {
+      HttpMethod that = (HttpMethod) obj;
+      return Objects.equals(name(), that.name());
+    }
+    return false;
+  }
+
+  @Override
+  public String toString() {
+    return name();
+  }
+}
diff --git a/src/main/java/io/vertx/core/http/impl/HttpUtils.java b/src/main/java/io/vertx/core/http/impl/HttpUtils.java
--- a/src/main/java/io/vertx/core/http/impl/HttpUtils.java
+++ b/src/main/java/io/vertx/core/http/impl/HttpUtils.java
@@ -667,41 +667,6 @@ static String determineContentEncoding(Http2Headers headers) {
     return null;
   }
 
-  static HttpMethod toNettyHttpMethod(io.vertx.core.http.HttpMethod method, String rawMethod) {
-    switch (method) {
-      case CONNECT: {
-        return HttpMethod.CONNECT;
-      }
-      case GET: {
-        return HttpMethod.GET;
-      }
-      case PUT: {
-        return HttpMethod.PUT;
-      }
-      case POST: {
-        return HttpMethod.POST;
-      }
-      case DELETE: {
-        return HttpMethod.DELETE;
-      }
-      case HEAD: {
-        return HttpMethod.HEAD;
-      }
-      case OPTIONS: {
-        return HttpMethod.OPTIONS;
-      }
-      case TRACE: {
-        return HttpMethod.TRACE;
-      }
-      case PATCH: {
-        return HttpMethod.PATCH;
-      }
-      default: {
-        return HttpMethod.valueOf(rawMethod);
-      }
-    }
-  }
-
   static HttpVersion toNettyHttpVersion(io.vertx.core.http.HttpVersion version) {
     switch (version) {
       case HTTP_1_0: {
@@ -716,11 +681,7 @@ static HttpVersion toNettyHttpVersion(io.vertx.core.http.HttpVersion version) {
   }
 
   static io.vertx.core.http.HttpMethod toVertxMethod(String method) {
-    try {
-      return io.vertx.core.http.HttpMethod.valueOf(method);
-    } catch (IllegalArgumentException e) {
-      return io.vertx.core.http.HttpMethod.OTHER;
-    }
+    return io.vertx.core.http.HttpMethod.valueOf(method);
   }
 
   private static final AsciiString TIMEOUT_EQ = AsciiString.of("timeout=");


Test patch:
diff --git a/src/test/java/io/vertx/core/http/Http1xTest.java b/src/test/java/io/vertx/core/http/Http1xTest.java
--- a/src/test/java/io/vertx/core/http/Http1xTest.java
+++ b/src/test/java/io/vertx/core/http/Http1xTest.java
@@ -3826,7 +3826,7 @@ public void testUnknownContentLengthIsSetToZeroWithHTTP_1_0() throws Exception {
   @Test
   public void testPartialH2CAmbiguousRequest() throws Exception {
     server.requestHandler(req -> {
-      assertEquals("POST", req.rawMethod());
+      assertEquals(HttpMethod.POST, req.method());
       testComplete();
     });
     Buffer fullRequest = Buffer.buffer("POST /whatever HTTP/1.1\r\n\r\n");
diff --git a/src/test/java/io/vertx/core/http/HttpMethodTest.java b/src/test/java/io/vertx/core/http/HttpMethodTest.java
new file mode 100644
--- /dev/null
+++ b/src/test/java/io/vertx/core/http/HttpMethodTest.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (c) 2011-2019 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ * which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+ */
+package io.vertx.core.http;
+
+import io.vertx.core.http.impl.HttpMethodImpl;
+import org.junit.Test;
+
+import java.util.Arrays;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.fail;
+
+public class HttpMethodTest {
+
+  @Test
+  public void testConstantNames() {
+    assertEquals("GET", HttpMethod.GET.name());
+    assertEquals("POST", HttpMethod.POST.name());
+    assertEquals("PUT", HttpMethod.PUT.name());
+    assertEquals("HEAD", HttpMethod.HEAD.name());
+    assertEquals("CONNECT", HttpMethod.CONNECT.name());
+    assertEquals("DELETE", HttpMethod.DELETE.name());
+    assertEquals("OPTIONS", HttpMethod.OPTIONS.name());
+    assertEquals("PATCH", HttpMethod.PATCH.name());
+    assertEquals("TRACE", HttpMethod.TRACE.name());
+  }
+
+  @Test
+  public void testConstants() {
+    for (HttpMethod method : Arrays.asList(
+      HttpMethod.GET,
+      HttpMethod.POST,
+      HttpMethod.HEAD,
+      HttpMethod.PUT,
+      HttpMethod.CONNECT,
+      HttpMethod.DELETE,
+      HttpMethod.OPTIONS,
+      HttpMethod.PATCH,
+      HttpMethod.TRACE
+      )) {
+      assertSame(HttpMethod.valueOf(method.name()), method);
+      assertSame(method.name(), method.toString());
+    }
+  }
+
+  @Test
+  public void testInvalidValueOf() {
+    for (String method : Arrays.asList("", " ")) {
+      try {
+        HttpMethod.valueOf(method);
+        fail();
+      } catch (IllegalArgumentException ignore) {
+      }
+    }
+    try {
+      HttpMethod.valueOf(null);
+      fail();
+    } catch (NullPointerException ignore) {
+    }
+  }
+
+  @Test
+  public void testValueOf() {
+    HttpMethod m1 = HttpMethod.valueOf("foo");
+    HttpMethod m2 = HttpMethod.valueOf("foo");
+    assertEquals("foo", m1.name());
+    assertEquals("foo", m1.toString());
+    assertNotSame(m1, m2);
+    assertEquals(m1.hashCode(), m2.hashCode());
+    assertEquals(m1, m2);
+  }
+
+  @Test
+  public void testCaseSensitive() {
+    HttpMethod m1 = HttpMethod.valueOf("Foo");
+    HttpMethod m2 = HttpMethod.valueOf("foo");
+    assertEquals("Foo", m1.name());
+    assertEquals("Foo", m1.toString());
+    assertNotSame(m1, m2);
+    assertNotEquals(m1.hashCode(), m2.hashCode());
+    assertNotEquals(m1, m2);
+  }
+
+  @Test
+  public void testNettyInterop() {
+    assertSame(HttpMethodImpl.toNetty(HttpMethod.GET), io.netty.handler.codec.http.HttpMethod.GET);
+    assertSame(HttpMethodImpl.toNetty(HttpMethod.POST), io.netty.handler.codec.http.HttpMethod.POST);
+    assertSame(HttpMethodImpl.toNetty(HttpMethod.PUT), io.netty.handler.codec.http.HttpMethod.PUT);
+    assertSame(HttpMethodImpl.toNetty(HttpMethod.HEAD), io.netty.handler.codec.http.HttpMethod.HEAD);
+    assertSame(HttpMethodImpl.toNetty(HttpMethod.CONNECT), io.netty.handler.codec.http.HttpMethod.CONNECT);
+    assertSame(HttpMethodImpl.toNetty(HttpMethod.DELETE), io.netty.handler.codec.http.HttpMethod.DELETE);
+    assertSame(HttpMethodImpl.toNetty(HttpMethod.OPTIONS), io.netty.handler.codec.http.HttpMethod.OPTIONS);
+    assertSame(HttpMethodImpl.toNetty(HttpMethod.PATCH), io.netty.handler.codec.http.HttpMethod.PATCH);
+    assertSame(HttpMethodImpl.toNetty(HttpMethod.TRACE), io.netty.handler.codec.http.HttpMethod.TRACE);
+    assertSame(HttpMethodImpl.fromNetty(io.netty.handler.codec.http.HttpMethod.GET), HttpMethod.GET);
+    assertSame(HttpMethodImpl.fromNetty(io.netty.handler.codec.http.HttpMethod.valueOf("GET")), HttpMethod.GET);
+    assertSame(HttpMethodImpl.fromNetty(io.netty.handler.codec.http.HttpMethod.POST), HttpMethod.POST);
+    assertSame(HttpMethodImpl.fromNetty(io.netty.handler.codec.http.HttpMethod.valueOf("POST")), HttpMethod.POST);
+    assertSame(HttpMethodImpl.fromNetty(io.netty.handler.codec.http.HttpMethod.PUT), HttpMethod.PUT);
+    assertSame(HttpMethodImpl.fromNetty(io.netty.handler.codec.http.HttpMethod.valueOf("PUT")), HttpMethod.PUT);
+    assertSame(HttpMethodImpl.fromNetty(io.netty.handler.codec.http.HttpMethod.HEAD), HttpMethod.HEAD);
+    assertSame(HttpMethodImpl.fromNetty(io.netty.handler.codec.http.HttpMethod.CONNECT), HttpMethod.CONNECT);
+    assertSame(HttpMethodImpl.fromNetty(io.netty.handler.codec.http.HttpMethod.DELETE), HttpMethod.DELETE);
+    assertSame(HttpMethodImpl.fromNetty(io.netty.handler.codec.http.HttpMethod.OPTIONS), HttpMethod.OPTIONS);
+    assertSame(HttpMethodImpl.fromNetty(io.netty.handler.codec.http.HttpMethod.PATCH), HttpMethod.PATCH);
+    assertSame(HttpMethodImpl.fromNetty(io.netty.handler.codec.http.HttpMethod.TRACE), HttpMethod.TRACE);
+    assertEquals(HttpMethodImpl.toNetty(HttpMethod.valueOf("foo")).name(), "foo");
+    assertEquals(HttpMethodImpl.fromNetty(io.netty.handler.codec.http.HttpMethod.valueOf("foo")).name(), "foo");
+  }
+}
diff --git a/src/test/java/io/vertx/core/http/HttpTest.java b/src/test/java/io/vertx/core/http/HttpTest.java
--- a/src/test/java/io/vertx/core/http/HttpTest.java
+++ b/src/test/java/io/vertx/core/http/HttpTest.java
@@ -3494,26 +3494,15 @@ public void testDumpManyRequestsOnQueue() throws Exception {
     await();
   }
 
-  @Test
-  public void testOtherMethodWithRawMethod() {
-    try {
-      client.request(HttpMethod.OTHER, testAddress, DEFAULT_HTTP_PORT, DEFAULT_HTTP_HOST, "/somepath", resp -> {
-      }).end();
-      fail();
-    } catch (IllegalStateException expected) {
-    }
-  }
-
   @Test
   public void testOtherMethodRequest() {
     server.requestHandler(r -> {
-      assertEquals(HttpMethod.OTHER, r.method());
-      assertEquals("COPY", r.rawMethod());
+      assertEquals("COPY", r.method().name());
       r.response().end();
     }).listen(testAddress, onSuccess(s -> {
-      client.request(HttpMethod.OTHER, testAddress, DEFAULT_HTTP_PORT, DEFAULT_HTTP_HOST, "/somepath", onSuccess(resp -> {
+      client.request(HttpMethod.valueOf("COPY"), testAddress, DEFAULT_HTTP_PORT, DEFAULT_HTTP_HOST, "/somepath", onSuccess(resp -> {
         testComplete();
-      })).setRawMethod("COPY").end();
+      })).end();
     }));
     await();
   }
diff --git a/src/test/java/io/vertx/core/json/JsonCodecTest.java b/src/test/java/io/vertx/core/json/JsonCodecTest.java
--- a/src/test/java/io/vertx/core/json/JsonCodecTest.java
+++ b/src/test/java/io/vertx/core/json/JsonCodecTest.java
@@ -12,7 +12,7 @@
 
 import com.fasterxml.jackson.core.type.TypeReference;
 import io.vertx.core.buffer.Buffer;
-import io.vertx.core.http.HttpMethod;
+import io.vertx.core.http.WebsocketVersion;
 import io.vertx.core.impl.Utils;
 import io.vertx.core.json.jackson.DatabindCodec;
 import io.vertx.core.json.jackson.JacksonCodec;
@@ -401,9 +401,9 @@ public void testDecodeValue() {
   @Test
   public void testEnumValue() {
     // just a random enum
-    Buffer json = mapper.toBuffer(HttpMethod.CONNECT);
+    Buffer json = mapper.toBuffer(WebsocketVersion.V13);
     assertNotNull(json);
-    assertEquals("\"CONNECT\"", json.toString());
+    assertEquals("\"V13\"", json.toString());
   }
 
   @Test