Repository: netty/netty (Java)

Issue:
Http2StreamFrameToHttpObjectCodec is marked @Sharable but is mutated by channel specific state
### Expected behavior

The `Http2StreamFrameToHttpObjectCodec` is either _not_ `@Sharable` _or_ it doesn't mutate its `scheme` field when added to a pipeline as it currently does [here](https://github.com/netty/netty/blob/4.1/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2StreamFrameToHttpObjectCodec.java#L221). 

### Netty version

4.1 as of SHA 8a24df88a4ff55.


Hints:
@bryce-anderson ouch... yes this is not good :( That said I suspect there will most likely not a lot of problems in real world use-cases as you either use SSL or not for all Channel (sharing between different parent Channels seems very unlikely).

That said we should fix. I guess an attribute on the parent channel should do. 
I also don't think it's a serious problem. In Finagle we simply make one per-channel. The only reason I found this is what I was about to cache instances of these and happened to notice that the `scheme` field was mutated after construction.

I'll put up a patch.

Patch:
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2StreamFrameToHttpObjectCodec.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2StreamFrameToHttpObjectCodec.java
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2StreamFrameToHttpObjectCodec.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2StreamFrameToHttpObjectCodec.java
@@ -41,6 +41,8 @@
 import io.netty.handler.codec.http.HttpVersion;
 import io.netty.handler.codec.http.LastHttpContent;
 import io.netty.handler.ssl.SslHandler;
+import io.netty.util.Attribute;
+import io.netty.util.AttributeKey;
 import io.netty.util.internal.UnstableApi;
 
 import java.util.List;
@@ -57,16 +59,17 @@
 @UnstableApi
 @Sharable
 public class Http2StreamFrameToHttpObjectCodec extends MessageToMessageCodec<Http2StreamFrame, HttpObject> {
+
+    private static final AttributeKey<HttpScheme> SCHEME_ATTR_KEY =
+        AttributeKey.valueOf(HttpScheme.class, "STREAMFRAMECODEC_SCHEME");
+
     private final boolean isServer;
     private final boolean validateHeaders;
 
-    private HttpScheme scheme;
-
     public Http2StreamFrameToHttpObjectCodec(final boolean isServer,
                                              final boolean validateHeaders) {
         this.isServer = isServer;
         this.validateHeaders = validateHeaders;
-        scheme = HttpScheme.HTTP;
     }
 
     public Http2StreamFrameToHttpObjectCodec(final boolean isServer) {
@@ -154,7 +157,7 @@ protected void encode(ChannelHandlerContext ctx, HttpObject obj, List<Object> ou
             final HttpResponse res = (HttpResponse) obj;
             if (res.status().equals(HttpResponseStatus.CONTINUE)) {
                 if (res instanceof FullHttpResponse) {
-                    final Http2Headers headers = toHttp2Headers(res);
+                    final Http2Headers headers = toHttp2Headers(ctx, res);
                     out.add(new DefaultHttp2HeadersFrame(headers, false));
                     return;
                 } else {
@@ -165,7 +168,7 @@ protected void encode(ChannelHandlerContext ctx, HttpObject obj, List<Object> ou
         }
 
         if (obj instanceof HttpMessage) {
-            Http2Headers headers = toHttp2Headers((HttpMessage) obj);
+            Http2Headers headers = toHttp2Headers(ctx, (HttpMessage) obj);
             boolean noMoreFrames = false;
             if (obj instanceof FullHttpMessage) {
                 FullHttpMessage full = (FullHttpMessage) obj;
@@ -184,11 +187,11 @@ protected void encode(ChannelHandlerContext ctx, HttpObject obj, List<Object> ou
         }
     }
 
-    private Http2Headers toHttp2Headers(final HttpMessage msg) {
+    private Http2Headers toHttp2Headers(final ChannelHandlerContext ctx, final HttpMessage msg) {
         if (msg instanceof HttpRequest) {
             msg.headers().set(
                     HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(),
-                    scheme.name());
+                    connectionScheme(ctx));
         }
 
         return HttpConversionUtil.toHttp2Headers(msg, validateHeaders);
@@ -213,17 +216,35 @@ private FullHttpMessage newFullMessage(final int id,
     public void handlerAdded(final ChannelHandlerContext ctx) throws Exception {
         super.handlerAdded(ctx);
 
-        // this handler is typically used on an Http2StreamChannel. at this
+        // this handler is typically used on an Http2StreamChannel. At this
         // stage, ssl handshake should've been established. checking for the
         // presence of SslHandler in the parent's channel pipeline to
         // determine the HTTP scheme should suffice, even for the case where
         // SniHandler is used.
-        scheme = isSsl(ctx) ? HttpScheme.HTTPS : HttpScheme.HTTP;
+        final Attribute<HttpScheme> schemeAttribute = connectionSchemeAttribute(ctx);
+        if (schemeAttribute.get() == null) {
+            final HttpScheme scheme = isSsl(ctx) ? HttpScheme.HTTPS : HttpScheme.HTTP;
+            schemeAttribute.set(scheme);
+        }
     }
 
     protected boolean isSsl(final ChannelHandlerContext ctx) {
-        final Channel ch = ctx.channel();
-        final Channel connChannel = (ch instanceof Http2StreamChannel) ? ch.parent() : ch;
+        final Channel connChannel = connectionChannel(ctx);
         return null != connChannel.pipeline().get(SslHandler.class);
     }
+
+    private static HttpScheme connectionScheme(ChannelHandlerContext ctx) {
+        final HttpScheme scheme = connectionSchemeAttribute(ctx).get();
+        return scheme == null ? HttpScheme.HTTP : scheme;
+    }
+
+    private static Attribute<HttpScheme> connectionSchemeAttribute(ChannelHandlerContext ctx) {
+        final Channel ch = connectionChannel(ctx);
+        return ch.attr(SCHEME_ATTR_KEY);
+    }
+
+    private static Channel connectionChannel(ChannelHandlerContext ctx) {
+        final Channel ch = ctx.channel();
+        return ch instanceof Http2StreamChannel ? ch.parent() : ch;
+    }
 }


Test patch:
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2StreamFrameToHttpObjectCodecTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2StreamFrameToHttpObjectCodecTest.java
--- a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2StreamFrameToHttpObjectCodecTest.java
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2StreamFrameToHttpObjectCodecTest.java
@@ -19,6 +19,7 @@
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.ByteBufAllocator;
 import io.netty.buffer.Unpooled;
+import io.netty.channel.ChannelHandler;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelOutboundHandlerAdapter;
 import io.netty.channel.ChannelPromise;
@@ -871,4 +872,65 @@ public void testPassThroughOtherAsClient() throws Exception {
             frame.release();
         }
     }
+
+    @Test
+    public void testIsSharableBetweenChannels() throws Exception {
+        final Queue<Http2StreamFrame> frames = new ConcurrentLinkedQueue<Http2StreamFrame>();
+        final ChannelHandler sharedHandler = new Http2StreamFrameToHttpObjectCodec(false);
+
+        final SslContext ctx = SslContextBuilder.forClient().sslProvider(SslProvider.JDK).build();
+        EmbeddedChannel tlsCh = new EmbeddedChannel(ctx.newHandler(ByteBufAllocator.DEFAULT),
+            new ChannelOutboundHandlerAdapter() {
+                @Override
+                public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
+                    if (msg instanceof Http2StreamFrame) {
+                        frames.add((Http2StreamFrame) msg);
+                        promise.setSuccess();
+                    } else {
+                        ctx.write(msg, promise);
+                    }
+                }
+            }, sharedHandler);
+
+        EmbeddedChannel plaintextCh = new EmbeddedChannel(
+            new ChannelOutboundHandlerAdapter() {
+                @Override
+                public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
+                    if (msg instanceof Http2StreamFrame) {
+                        frames.add((Http2StreamFrame) msg);
+                        promise.setSuccess();
+                    } else {
+                        ctx.write(msg, promise);
+                    }
+                }
+            }, sharedHandler);
+
+        FullHttpRequest req = new DefaultFullHttpRequest(
+            HttpVersion.HTTP_1_1, HttpMethod.GET, "/hello/world");
+        assertTrue(tlsCh.writeOutbound(req));
+        assertTrue(tlsCh.finishAndReleaseAll());
+
+        Http2HeadersFrame headersFrame = (Http2HeadersFrame) frames.poll();
+        Http2Headers headers = headersFrame.headers();
+
+        assertThat(headers.scheme().toString(), is("https"));
+        assertThat(headers.method().toString(), is("GET"));
+        assertThat(headers.path().toString(), is("/hello/world"));
+        assertTrue(headersFrame.isEndStream());
+        assertNull(frames.poll());
+
+        // Run the plaintext channel
+        req = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/hello/world");
+        assertFalse(plaintextCh.writeOutbound(req));
+        assertFalse(plaintextCh.finishAndReleaseAll());
+
+        headersFrame = (Http2HeadersFrame) frames.poll();
+        headers = headersFrame.headers();
+
+        assertThat(headers.scheme().toString(), is("http"));
+        assertThat(headers.method().toString(), is("GET"));
+        assertThat(headers.path().toString(), is("/hello/world"));
+        assertTrue(headersFrame.isEndStream());
+        assertNull(frames.poll());
+    }
 }