Repository: netty/netty
Issue: HTTP/2: Many specification compliancy issues

I ran [h2spec](https://github.com/summerwind/h2spec) against the hello world HTTP/2 server (4.1 branch, with removed HTTP upgrade logic). The result: 70 tests, 37 passed, 1 skipped, **32 failed**. 

From skimming over the list of errors, the failed tests seem to be mostly due to Netty responding with wrong error codes. [Here](https://gist.github.com/buchgr/9b8bc81592258008641e20e6d96f61c2) is a detailed list of what went wrong. 

cc: @Scottmitch @nmittler @louiscryan @ejona86 


Issue comments:
@buchgr thanks for having a look! I think I should write a maven plugin to do this as part of our build just as we do with the autobahn test suite (web sockets)

@normanmaurer  ... and first fix all the errors 😜 

@buchgr great, thanks for looking at this! Yes ... fixing errors sgtm :stuck_out_tongue_winking_eye:

Working on a fix. Most errors seem pretty straightfoward... 

Thanks a lot!

> Am 29.08.2016 um 16:01 schrieb Jakob Buchgraber notifications@github.com:
> 
> Working on a fix. Most errors seem pretty straightfoward...
> 
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub, or mute the thread.

Everybody here, my platform in addition to use asynchronous circuit also used in the synchronous circuit, and I did not find netty synchronous architecture, thinking, I realized a framework, called net Rio, it to NiO based synchronization response mechanism, basic to meet the needs of our platform. The project was open source, hope you want to join

thanks for uncovering and patching @buchgr !

Running `h2spec` now:  70 tests, 48 passed, 1 skipped, 21 failed

When increasing the `DEFAULT_MAX_HEADER_SIZE` to 64KiB, 6 fewer tests fail. Specfically those in `6.10 CONTINUATION`, as this test sends large HEADERS / CONTINUATION frames. It's totally legit for a HTTP/2 implementation to not support such large HEADERS though https://github.com/netty/netty/issues/5774#issuecomment-243618794 ...

May I ask that if I could help on this issue?
I will pick up some simple case to fix and create issue and PR for that.
It seems that most failed test are occurred at http2 validation.
Thanks!
@chhsiao90 sure just open PRs, we love contributions 

Pull request review comments:
why this change ?
This will allocate a new Iterator on each loop. Consider using an old style for loop.
should we move this check above the for loop as the check is less expensive. 
toString() is not needed here.
use method chaining ?
should we maybe rename tis this `newHttp2HeadersWithPseudoHeaders()`
Oops will change that back
Do we need to check for multiple `:method` headers?
This is going to be relatively poor performing considering we're going to make a list for each mandatory header. Since the spec clearly defines which ones are mandatory could we just iterate through the header list in order and count them up, eg have `boolean methodObserved, pathObserved, ...` and make sure we don't do more than one and that we got all three?
Do we worry about the client side validation somewhere else?
Ah yeah good point! I should add that to h2spec as well
Iterating through the whole list would scale linearly with the number of headers whereas this version has a constant run time. If we assume the number of headers is most of the time small iterating would be more performant.

Ideally we would have a `count(name)` method on headers that would just count the headers with a given name, not convinced this usecase justify adding that to the API though.
If we use my strategy of having an `observed` variable for each critical header we should count no more than 4 as at 4 we'd hit an exceptional condition. This actually gets to another conformance requirement of the RFC ([section 8.1.2.1](https://http2.github.io/http2-spec/#rfc.section.8.1.2.1)) which is that we should make sure we don't have bonus pseudo headers: `Pseudo-header fields defined for requests MUST NOT appear in responses; pseudo-header fields defined for responses MUST NOT appear in requests.`
Nice, forgot about this requirement, will change it. 
(BTW 8.1.2.1 requirement is already checked in the HPackDecoder) 
Ah, sure enough. Do you think it would be good to consolidate the header validation somewhere? Maybe it's too late, but I find it a bit surprising that the `HpackDecoder` takes it upon itself to enforce session semantics, and it seems that it can only do a subset of them. (consolidation could be a different PR if done at all, I'm just throwing half baked ideas around so fft tell me why my suggestions are ridiculous 😄)
They validate at different levels: in `HPackdecoder` we check if the headerBlock is valid, here we check if all the headers together make sense, that being said it would be nice if it was done at a single place.

The problem is that `HPackDecoder` level validation needs to know the order of the headers, we lose the order once we return Http2Headers and  `Http2HeadersValidation` needs to know more about the context: is this an initial header or trailing one? It didn't seem right to push that down to hpackdecoder.
That's true regarding the order validation in hpack. Maybe we could shift request/response validation to where you're adding more validation logic, but probably not all of it and at a minimum this isn't obviously right.
Baby steps, h2spec only validate server specification for now.
This check was moved to `Http2HeadersValidator` as it doesn't rely on the ordering
I'm not sure we want to put this check behind the `validateHeaders` check as it was always performed before. Someone else may have a different opinion though.
Since this was moved out of HpackDecoder I think we should now validate the request pseudo headers as it leaves a hole otherwise.
Since we're only validating the request specific headers I think we can make this another `else` clause in the branches below.
H2 headers must always have lower case keys and pseudo-headers should be no exception, so we should be able to omit the `IngoreCase`.
Even if we don't plan on doing the client side now we should probably prepare for that by making the name reflect that it is request specific.
Oh yeah good point, will wait on other opinion for now
This comment and the next one make me think we should probably leave this check in HPackDecoder. 🤔 
I moved that out of the condition in the end.
spelling: specific

Patch that resolved the issue:
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/AbstractHttp2ConnectionHandlerBuilder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/AbstractHttp2ConnectionHandlerBuilder.java
index f262b114f09..557341c1897 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/AbstractHttp2ConnectionHandlerBuilder.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/AbstractHttp2ConnectionHandlerBuilder.java
@@ -535,8 +535,8 @@ private T buildFromConnection(Http2Connection connection) {
             encoder = new StreamBufferingEncoder(encoder);
         }
 
-        DefaultHttp2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, reader,
-                promisedRequestVerifier(), isAutoAckSettingsFrame(), isAutoAckPingFrame());
+        Http2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, reader,
+                promisedRequestVerifier(), isAutoAckSettingsFrame(), isAutoAckPingFrame(), isValidateHeaders());
         return buildFromCodec(decoder, encoder);
     }
 
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionDecoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionDecoder.java
index 09f16c0a4bd..01a5f3831cb 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionDecoder.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionDecoder.java
@@ -31,6 +31,9 @@
 import static io.netty.handler.codec.http2.Http2Error.STREAM_CLOSED;
 import static io.netty.handler.codec.http2.Http2Exception.connectionError;
 import static io.netty.handler.codec.http2.Http2Exception.streamError;
+import static io.netty.handler.codec.http2.Http2HeadersValidator.validateConnectionSpecificHeaders;
+import static io.netty.handler.codec.http2.Http2HeadersValidator.validateRequestPseudoHeaders;
+import static io.netty.handler.codec.http2.Http2HeadersValidator.validateResponsePseudoHeaders;
 import static io.netty.handler.codec.http2.Http2PromisedRequestVerifier.ALWAYS_VERIFY;
 import static io.netty.handler.codec.http2.Http2Stream.State.CLOSED;
 import static io.netty.handler.codec.http2.Http2Stream.State.HALF_CLOSED_REMOTE;
@@ -59,6 +62,7 @@ public class DefaultHttp2ConnectionDecoder implements Http2ConnectionDecoder {
     private final Http2PromisedRequestVerifier requestVerifier;
     private final Http2SettingsReceivedConsumer settingsReceivedConsumer;
     private final boolean autoAckPing;
+    private final boolean validateHeaders;
 
     public DefaultHttp2ConnectionDecoder(Http2Connection connection,
                                          Http2ConnectionEncoder encoder,
@@ -93,6 +97,15 @@ public DefaultHttp2ConnectionDecoder(Http2Connection connection,
         this(connection, encoder, frameReader, requestVerifier, autoAckSettings, true);
     }
 
+    public DefaultHttp2ConnectionDecoder(Http2Connection connection,
+                                         Http2ConnectionEncoder encoder,
+                                         Http2FrameReader frameReader,
+                                         Http2PromisedRequestVerifier requestVerifier,
+                                         boolean autoAckSettings,
+                                         boolean autoAckPing) {
+        this(connection, encoder, frameReader, requestVerifier, autoAckSettings, autoAckPing, false);
+    }
+
     /**
      * Create a new instance.
      * @param connection The {@link Http2Connection} associated with this decoder.
@@ -113,7 +126,8 @@ public DefaultHttp2ConnectionDecoder(Http2Connection connection,
                                          Http2FrameReader frameReader,
                                          Http2PromisedRequestVerifier requestVerifier,
                                          boolean autoAckSettings,
-                                         boolean autoAckPing) {
+                                         boolean autoAckPing,
+                                         boolean validateHeaders) {
         this.autoAckPing = autoAckPing;
         if (autoAckSettings) {
             settingsReceivedConsumer = null;
@@ -128,6 +142,7 @@ public DefaultHttp2ConnectionDecoder(Http2Connection connection,
         this.frameReader = checkNotNull(frameReader, "frameReader");
         this.encoder = checkNotNull(encoder, "encoder");
         this.requestVerifier = checkNotNull(requestVerifier, "requestVerifier");
+        this.validateHeaders = validateHeaders;
         if (connection.local().flowController() == null) {
             connection.local().flowController(new DefaultHttp2LocalFlowController(connection));
         }
@@ -344,6 +359,10 @@ public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers
                                   streamId, endOfStream, stream.state());
             }
 
+            if (validateHeaders) {
+                validateHeaders(streamId, headers, stream);
+            }
+
             switch (stream.state()) {
                 case RESERVED_REMOTE:
                     stream.open(endOfStream);
@@ -631,6 +650,18 @@ private void verifyStreamMayHaveExisted(int streamId) throws Http2Exception {
                 throw connectionError(PROTOCOL_ERROR, "Stream %d does not exist", streamId);
             }
         }
+
+        private void validateHeaders(int streamId, Http2Headers headers, Http2Stream stream) throws Http2Exception {
+            if (connection.isServer()) {
+                if (!stream.isHeadersReceived() || stream.state() == HALF_CLOSED_REMOTE) {
+                    validateRequestPseudoHeaders(headers, streamId);
+                }
+            } else {
+                validateResponsePseudoHeaders(headers, streamId);
+            }
+
+            validateConnectionSpecificHeaders(headers, streamId);
+        }
     }
 
     private final class PrefaceFrameListener implements Http2FrameListener {
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackDecoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackDecoder.java
index 6070bc5f98b..1499d807527 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackDecoder.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackDecoder.java
@@ -393,13 +393,8 @@ private static HeaderType validate(int streamId, CharSequence name,
                 throw streamError(streamId, PROTOCOL_ERROR, "Invalid HTTP/2 pseudo-header '%s' encountered.", name);
             }
 
-            final HeaderType currentHeaderType = pseudoHeader.isRequestOnly() ?
+            return pseudoHeader.isRequestOnly() ?
                     HeaderType.REQUEST_PSEUDO_HEADER : HeaderType.RESPONSE_PSEUDO_HEADER;
-            if (previousHeaderType != null && currentHeaderType != previousHeaderType) {
-                throw streamError(streamId, PROTOCOL_ERROR, "Mix of request and response pseudo-headers.");
-            }
-
-            return currentHeaderType;
         }
 
         return HeaderType.REGULAR_HEADER;
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2HeadersValidator.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2HeadersValidator.java
new file mode 100644
index 00000000000..fa4ed9e115c
--- /dev/null
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2HeadersValidator.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2018 The Netty Project
+ *
+ * The Netty Project licenses this file to you under the Apache License, version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License. You may obtain a
+ * copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.netty.handler.codec.http2;
+
+import io.netty.handler.codec.http.HttpMethod;
+import io.netty.handler.codec.http2.Http2Headers.PseudoHeaderName;
+import io.netty.util.AsciiString;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map.Entry;
+
+import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;
+import static io.netty.handler.codec.http.HttpHeaderNames.KEEP_ALIVE;
+import static io.netty.handler.codec.http.HttpHeaderNames.TE;
+import static io.netty.handler.codec.http.HttpHeaderNames.TRANSFER_ENCODING;
+import static io.netty.handler.codec.http.HttpHeaderNames.UPGRADE;
+import static io.netty.handler.codec.http.HttpHeaderValues.TRAILERS;
+import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
+import static io.netty.handler.codec.http2.Http2Exception.streamError;
+import static io.netty.handler.codec.http2.Http2Headers.PseudoHeaderName.METHOD;
+import static io.netty.handler.codec.http2.Http2Headers.PseudoHeaderName.PATH;
+import static io.netty.handler.codec.http2.Http2Headers.PseudoHeaderName.SCHEME;
+import static io.netty.handler.codec.http2.Http2Headers.PseudoHeaderName.hasPseudoHeaderFormat;
+
+final class Http2HeadersValidator {
+
+    private static final List<AsciiString> connectionSpecificHeaders = Collections.unmodifiableList(
+            Arrays.asList(CONNECTION, TRANSFER_ENCODING, KEEP_ALIVE, UPGRADE));
+
+    private Http2HeadersValidator() {
+    }
+
+    /**
+     * Validates connection-specific headers according to
+     * <a href="https://tools.ietf.org/html/rfc7540#section-8.1.2.2">RFC7540, section-8.1.2.2</a>
+     */
+    static void validateConnectionSpecificHeaders(Http2Headers headers, int streamId) throws Http2Exception {
+        for (int i = 0; i < connectionSpecificHeaders.size(); i++) {
+            final AsciiString header = connectionSpecificHeaders.get(i);
+            if (headers.contains(header)) {
+                throw streamError(streamId, PROTOCOL_ERROR,
+                                  "Connection-specific headers like [%s] must not be used with HTTP/2.", header);
+            }
+        }
+
+        final CharSequence teHeader = headers.get(TE);
+        if (teHeader != null && !AsciiString.contentEqualsIgnoreCase(teHeader, TRAILERS)) {
+            throw streamError(streamId, PROTOCOL_ERROR,
+                              "TE header must not contain any value other than \"%s\"", TRAILERS);
+        }
+    }
+
+    /**
+     * Validates response pseudo-header fields
+     */
+    static void validateResponsePseudoHeaders(Http2Headers headers, int streamId) throws Http2Exception {
+        for (Entry<CharSequence, CharSequence> entry : headers) {
+            final CharSequence key = entry.getKey();
+            if (!hasPseudoHeaderFormat(key)) {
+                // We know that pseudo header appears first so we can stop
+                // looking once we get to the first non pseudo headers.
+                break;
+            }
+
+            final PseudoHeaderName pseudoHeader = PseudoHeaderName.getPseudoHeader(key);
+            if (pseudoHeader.isRequestOnly()) {
+                throw streamError(streamId, PROTOCOL_ERROR,
+                                  "Request pseudo-header [%s] is not allowed in a response.", key);
+            }
+        }
+    }
+
+    /**
+     * Validates request pseudo-header fields according to
+     * <a href="https://tools.ietf.org/html/rfc7540#section-8.1.2.3">RFC7540, section-8.1.2.3</a>
+     */
+    static void validateRequestPseudoHeaders(Http2Headers headers, int streamId) throws Http2Exception {
+        final CharSequence method = headers.get(METHOD.value());
+        if (method == null) {
+            throw streamError(streamId, PROTOCOL_ERROR,
+                              "Mandatory header [:method] is missing.");
+        }
+
+        if (HttpMethod.CONNECT.asciiName().contentEqualsIgnoreCase(method)) {
+            if (headers.contains(SCHEME.value())) {
+                throw streamError(streamId, PROTOCOL_ERROR,
+                                  "Header [:scheme] must be omitted when using CONNECT method.");
+            }
+
+            if (headers.contains(PATH.value())) {
+                throw streamError(streamId, PROTOCOL_ERROR,
+                                  "Header [:path] must be omitted when using CONNECT method.");
+            }
+
+            if (headers.getAll(METHOD.value()).size() > 1) {
+                throw streamError(streamId, PROTOCOL_ERROR,
+                                  "Header [:method] should have a unique value.");
+            }
+        } else {
+            final CharSequence path = headers.get(PATH.value());
+            if (path != null && path.length() == 0) {
+                throw streamError(streamId, PROTOCOL_ERROR, "[:path] header cannot be empty.");
+            }
+
+            int methodHeadersCount = 0;
+            int pathHeadersCount = 0;
+            int schemeHeadersCount = 0;
+            for (Entry<CharSequence, CharSequence> entry : headers) {
+                final CharSequence key = entry.getKey();
+                if (!hasPseudoHeaderFormat(key)) {
+                    // We know that pseudo header appears first so we can stop
+                    // looking once we get to the first non pseudo headers.
+                    break;
+                }
+
+                final PseudoHeaderName pseudoHeader = PseudoHeaderName.getPseudoHeader(key);
+                if (METHOD.value().contentEquals(key)) {
+                    methodHeadersCount++;
+                } else if (PATH.value().contentEquals(key)) {
+                    pathHeadersCount++;
+                } else if (SCHEME.value().contentEquals(key)) {
+                    schemeHeadersCount++;
+                } else if (!pseudoHeader.isRequestOnly()) {
+                    throw streamError(streamId, PROTOCOL_ERROR,
+                                      "Response pseudo-header [%s] is not allowed in a request.", key);
+                }
+            }
+
+            validatePseudoHeaderCount(streamId, methodHeadersCount, METHOD);
+            validatePseudoHeaderCount(streamId, pathHeadersCount, PATH);
+            validatePseudoHeaderCount(streamId, schemeHeadersCount, SCHEME);
+        }
+    }
+
+    private static void validatePseudoHeaderCount(int streamId, int valueCount, PseudoHeaderName headerName)
+            throws Http2Exception {
+        if (valueCount == 0) {
+            throw streamError(streamId, PROTOCOL_ERROR,
+                              "Mandatory header [%s] is missing.", headerName.value());
+        } else if (valueCount > 1) {
+            throw streamError(streamId, PROTOCOL_ERROR,
+                              "Header [%s] should have a unique value.", headerName.value());
+        }
+    }
+}
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpConversionUtil.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpConversionUtil.java
index 8ffbd285338..3aadce28ce9 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpConversionUtil.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpConversionUtil.java
@@ -391,9 +391,14 @@ public static Http2Headers toHttp2Headers(HttpMessage in, boolean validateHeader
         if (in instanceof HttpRequest) {
             HttpRequest request = (HttpRequest) in;
             URI requestTargetUri = URI.create(request.uri());
-            out.path(toHttp2Path(requestTargetUri));
             out.method(request.method().asciiName());
-            setHttp2Scheme(inHeaders, requestTargetUri, out);
+
+            // According to the spec https://tools.ietf.org/html/rfc7540#section-8.3 scheme and path
+            // should be omitted for CONNECT method
+            if (request.method() != HttpMethod.CONNECT) {
+                setHttp2Scheme(inHeaders, requestTargetUri, out);
+                out.path(toHttp2Path(requestTargetUri));
+            }
 
             if (!isOriginForm(requestTargetUri) && !isAsteriskForm(requestTargetUri)) {
                 // Attempt to take from HOST header before taking from the request-line


Test patch:
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/DataCompressionHttp2Test.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/DataCompressionHttp2Test.java
index 1d815a330cf..f3b5edffb9b 100644
--- a/codec-http2/src/test/java/io/netty/handler/codec/http2/DataCompressionHttp2Test.java
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/DataCompressionHttp2Test.java
@@ -70,6 +70,7 @@ public class DataCompressionHttp2Test {
     private static final AsciiString GET = new AsciiString("GET");
     private static final AsciiString POST = new AsciiString("POST");
     private static final AsciiString PATH = new AsciiString("/some/path");
+    private static final AsciiString SCHEME = new AsciiString("http");
 
     @Mock
     private Http2FrameListener serverListener;
@@ -144,7 +145,7 @@ public void teardown() throws InterruptedException {
     @Test
     public void justHeadersNoData() throws Exception {
         bootstrapEnv(0);
-        final Http2Headers headers = new DefaultHttp2Headers().method(GET).path(PATH)
+        final Http2Headers headers = new DefaultHttp2Headers().method(GET).path(PATH).scheme(SCHEME)
                 .set(HttpHeaderNames.CONTENT_ENCODING, HttpHeaderValues.GZIP);
 
         runInChannel(clientChannel, new Http2Runnable() {
@@ -165,7 +166,7 @@ public void gzipEncodingSingleEmptyMessage() throws Exception {
         final ByteBuf data = Unpooled.copiedBuffer(text.getBytes());
         bootstrapEnv(data.readableBytes());
         try {
-            final Http2Headers headers = new DefaultHttp2Headers().method(POST).path(PATH)
+            final Http2Headers headers = new DefaultHttp2Headers().method(POST).path(PATH).scheme(SCHEME)
                     .set(HttpHeaderNames.CONTENT_ENCODING, HttpHeaderValues.GZIP);
 
             runInChannel(clientChannel, new Http2Runnable() {
@@ -189,7 +190,7 @@ public void gzipEncodingSingleMessage() throws Exception {
         final ByteBuf data = Unpooled.copiedBuffer(text.getBytes());
         bootstrapEnv(data.readableBytes());
         try {
-            final Http2Headers headers = new DefaultHttp2Headers().method(POST).path(PATH)
+            final Http2Headers headers = new DefaultHttp2Headers().method(POST).path(PATH).scheme(SCHEME)
                     .set(HttpHeaderNames.CONTENT_ENCODING, HttpHeaderValues.GZIP);
 
             runInChannel(clientChannel, new Http2Runnable() {
@@ -215,7 +216,7 @@ public void gzipEncodingMultipleMessages() throws Exception {
         final ByteBuf data2 = Unpooled.copiedBuffer(text2.getBytes());
         bootstrapEnv(data1.readableBytes() + data2.readableBytes());
         try {
-            final Http2Headers headers = new DefaultHttp2Headers().method(POST).path(PATH)
+            final Http2Headers headers = new DefaultHttp2Headers().method(POST).path(PATH).scheme(SCHEME)
                     .set(HttpHeaderNames.CONTENT_ENCODING, HttpHeaderValues.GZIP);
 
             runInChannel(clientChannel, new Http2Runnable() {
@@ -243,7 +244,7 @@ public void deflateEncodingWriteLargeMessage() throws Exception {
         bootstrapEnv(BUFFER_SIZE);
         final ByteBuf data = Unpooled.wrappedBuffer(bytes);
         try {
-            final Http2Headers headers = new DefaultHttp2Headers().method(POST).path(PATH)
+            final Http2Headers headers = new DefaultHttp2Headers().method(POST).path(PATH).scheme(SCHEME)
                     .set(HttpHeaderNames.CONTENT_ENCODING, HttpHeaderValues.DEFLATE);
 
             runInChannel(clientChannel, new Http2Runnable() {
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionDecoderTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionDecoderTest.java
index 7e87d52893c..75cfbf43ea5 100644
--- a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionDecoderTest.java
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionDecoderTest.java
@@ -22,6 +22,7 @@
 import io.netty.channel.ChannelPromise;
 import io.netty.channel.DefaultChannelPromise;
 import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http2.Http2Headers.PseudoHeaderName;
 import junit.framework.AssertionFailedError;
 import org.junit.Before;
 import org.junit.Test;
@@ -38,9 +39,11 @@
 import static io.netty.buffer.Unpooled.wrappedBuffer;
 import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_PRIORITY_WEIGHT;
 import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
+import static io.netty.handler.codec.http2.Http2Stream.State.HALF_CLOSED_REMOTE;
 import static io.netty.handler.codec.http2.Http2Stream.State.IDLE;
 import static io.netty.handler.codec.http2.Http2Stream.State.OPEN;
 import static io.netty.handler.codec.http2.Http2Stream.State.RESERVED_REMOTE;
+import static io.netty.handler.codec.http2.Http2TestUtil.newHttp2HeadersWithRequestPseudoHeaders;
 import static io.netty.util.CharsetUtil.UTF_8;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
@@ -192,7 +195,8 @@ public Http2Stream answer(InvocationOnMock in) throws Throwable {
         when(ctx.newPromise()).thenReturn(promise);
         when(ctx.write(any())).thenReturn(future);
 
-        decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, reader);
+        decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, reader,
+                Http2PromisedRequestVerifier.ALWAYS_VERIFY, true, true, true);
         decoder.lifecycleManager(lifecycleManager);
         decoder.frameListener(listener);
 
@@ -492,6 +496,64 @@ public void headersReadForPromisedStreamShouldHalfOpenStream() throws Exception
                 eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false));
     }
 
+    @Test(expected = Http2Exception.class)
+    public void requestPseudoHeadersInResponseThrows() throws Exception {
+        when(connection.isServer()).thenReturn(false);
+        when(connection.stream(STREAM_ID)).thenReturn(null);
+        when(connection.streamMayHaveExisted(STREAM_ID)).thenReturn(false);
+        when(remote.createStream(eq(STREAM_ID), anyBoolean())).thenReturn(stream);
+        when(stream.state()).thenReturn(HALF_CLOSED_REMOTE);
+        final Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders();
+        decode().onHeadersRead(ctx, STREAM_ID, headers, 0, false);
+    }
+
+    @Test(expected = Http2Exception.class)
+    public void missingPseudoHeadersInLeadingHeaderThrows() throws Exception {
+        when(connection.isServer()).thenReturn(true);
+        when(connection.stream(STREAM_ID)).thenReturn(null);
+        when(connection.streamMayHaveExisted(STREAM_ID)).thenReturn(false);
+        when(remote.createStream(eq(STREAM_ID), anyBoolean())).thenReturn(stream);
+        when(stream.state()).thenReturn(HALF_CLOSED_REMOTE);
+        final Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders();
+        headers.remove(PseudoHeaderName.METHOD.value());
+        decode().onHeadersRead(ctx, STREAM_ID, headers, 0, false);
+    }
+
+    @Test
+    public void missingPseudoHeadersInLeadingHeaderShouldNotThrowsIfValidationDisabled() throws Exception {
+        when(connection.isServer()).thenReturn(true);
+        when(connection.stream(STREAM_ID)).thenReturn(null);
+        when(connection.streamMayHaveExisted(STREAM_ID)).thenReturn(false);
+        when(remote.createStream(eq(STREAM_ID), anyBoolean())).thenReturn(stream);
+        when(stream.state()).thenReturn(HALF_CLOSED_REMOTE);
+        final Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders();
+        headers.remove(PseudoHeaderName.METHOD.value());
+
+        decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, reader,
+                Http2PromisedRequestVerifier.ALWAYS_VERIFY, true, true, false);
+        decoder.lifecycleManager(lifecycleManager);
+        decoder.frameListener(listener);
+
+        // Simulate receiving the initial settings from the remote endpoint.
+        decode().onSettingsRead(ctx, new Http2Settings());
+        // Simulate receiving the SETTINGS ACK for the initial settings.
+        decode().onSettingsAckRead(ctx);
+
+        decode().onHeadersRead(ctx, STREAM_ID, headers, 0, false);
+    }
+
+    @Test
+    public void missingPseudoHeadersInTrailerHeaderDoesNotThrow() throws Exception {
+        when(connection.isServer()).thenReturn(true);
+        when(connection.stream(STREAM_ID)).thenReturn(stream);
+
+        decode().onHeadersRead(ctx, STREAM_ID, newHttp2HeadersWithRequestPseudoHeaders(), 0, false);
+
+        final Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders();
+        headers.remove(PseudoHeaderName.METHOD.value());
+        decode().onHeadersRead(ctx, STREAM_ID, headers, 0, true);
+    }
+
     @Test(expected = Http2Exception.class)
     public void trailersDoNotEndStreamThrows() throws Exception {
         decode().onHeadersRead(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, false);
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackDecoderTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackDecoderTest.java
index 7b26d90d170..7abbbe4598b 100644
--- a/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackDecoderTest.java
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackDecoderTest.java
@@ -575,46 +575,6 @@ public void disableHeaderValidation() throws Exception {
         }
     }
 
-    @Test
-    public void requestPseudoHeaderInResponse() throws Exception {
-        ByteBuf in = Unpooled.buffer(200);
-        try {
-            HpackEncoder hpackEncoder = new HpackEncoder(true);
-
-            Http2Headers toEncode = new DefaultHttp2Headers();
-            toEncode.add(":status", "200");
-            toEncode.add(":method", "GET");
-            hpackEncoder.encodeHeaders(1, in, toEncode, NEVER_SENSITIVE);
-
-            Http2Headers decoded = new DefaultHttp2Headers();
-
-            expectedException.expect(Http2Exception.StreamException.class);
-            hpackDecoder.decode(1, in, decoded, true);
-        } finally {
-            in.release();
-        }
-    }
-
-    @Test
-    public void responsePseudoHeaderInRequest() throws Exception {
-        ByteBuf in = Unpooled.buffer(200);
-        try {
-            HpackEncoder hpackEncoder = new HpackEncoder(true);
-
-            Http2Headers toEncode = new DefaultHttp2Headers();
-            toEncode.add(":method", "GET");
-            toEncode.add(":status", "200");
-            hpackEncoder.encodeHeaders(1, in, toEncode, NEVER_SENSITIVE);
-
-            Http2Headers decoded = new DefaultHttp2Headers();
-
-            expectedException.expect(Http2Exception.StreamException.class);
-            hpackDecoder.decode(1, in, decoded, true);
-        } finally {
-            in.release();
-        }
-    }
-
     @Test
     public void pseudoHeaderAfterRegularHeader() throws Exception {
         ByteBuf in = Unpooled.buffer(200);
@@ -644,7 +604,7 @@ public void failedValidationDoesntCorruptHpack() throws Exception {
 
             Http2Headers toEncode = new DefaultHttp2Headers();
             toEncode.add(":method", "GET");
-            toEncode.add(":status", "200");
+            toEncode.add(":unknownpseudoheader", "200");
             toEncode.add("foo", "bar");
             hpackEncoder.encodeHeaders(1, in1, toEncode, NEVER_SENSITIVE);
 
@@ -664,7 +624,7 @@ public void failedValidationDoesntCorruptHpack() throws Exception {
 
             assertEquals(3, decoded.size());
             assertEquals("GET", decoded.method().toString());
-            assertEquals("200", decoded.status().toString());
+            assertEquals("200", decoded.get(":unknownpseudoheader").toString());
             assertEquals("bar", decoded.get("foo").toString());
         } finally {
             in1.release();
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2HeadersValidatorTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2HeadersValidatorTest.java
new file mode 100644
index 00000000000..3f323a6ef05
--- /dev/null
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2HeadersValidatorTest.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2018 The Netty Project
+ *
+ * The Netty Project licenses this file to you under the Apache License, version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License. You may obtain a
+ * copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.netty.handler.codec.http2;
+
+import io.netty.handler.codec.http2.Http2Exception.StreamException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;
+import static io.netty.handler.codec.http.HttpHeaderNames.TE;
+import static io.netty.handler.codec.http2.Http2Headers.PseudoHeaderName.METHOD;
+import static io.netty.handler.codec.http2.Http2Headers.PseudoHeaderName.PATH;
+import static io.netty.handler.codec.http2.Http2Headers.PseudoHeaderName.SCHEME;
+import static io.netty.handler.codec.http2.Http2Headers.PseudoHeaderName.STATUS;
+import static io.netty.handler.codec.http2.Http2HeadersValidator.validateConnectionSpecificHeaders;
+import static io.netty.handler.codec.http2.Http2HeadersValidator.validateRequestPseudoHeaders;
+import static io.netty.handler.codec.http2.Http2TestUtil.newHttp2HeadersWithRequestPseudoHeaders;
+
+public class Http2HeadersValidatorTest {
+
+    private static final int STREAM_ID = 3;
+
+    @Rule
+    public final ExpectedException expectedException = ExpectedException.none();
+
+    @Test
+    public void validateConnectionSpecificHeadersShouldThrowIfConnectionHeaderPresent() throws Http2Exception {
+        expectedException.expect(StreamException.class);
+        expectedException.expectMessage("Connection-speficic headers like [connection] must not be used with HTTP");
+
+        final Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders();
+        headers.add(CONNECTION, "keep-alive");
+        validateConnectionSpecificHeaders(headers, STREAM_ID);
+    }
+
+    @Test
+    public void validateConnectionSpecificHeadersShouldThrowIfTeHeaderValueIsNotTrailers() throws Http2Exception {
+        expectedException.expect(StreamException.class);
+        expectedException.expectMessage("TE header must not contain any value other than \"trailers\"");
+
+        final Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders();
+        headers.add(TE, "trailers, deflate");
+        validateConnectionSpecificHeaders(headers, STREAM_ID);
+    }
+
+    @Test
+    public void validatePseudoHeadersShouldThrowWhenMethodHeaderIsMissing() throws Http2Exception {
+        expectedException.expect(StreamException.class);
+        expectedException.expectMessage("Mandatory header [:method] is missing.");
+
+        final Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders();
+        headers.remove(METHOD.value());
+        validateRequestPseudoHeaders(headers, STREAM_ID);
+    }
+
+    @Test
+    public void validatePseudoHeadersShouldThrowWhenPathHeaderIsMissing() throws Http2Exception {
+        expectedException.expect(StreamException.class);
+        expectedException.expectMessage("Mandatory header [:path] is missing.");
+
+        final Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders();
+        headers.remove(PATH.value());
+        validateRequestPseudoHeaders(headers, STREAM_ID);
+    }
+
+    @Test
+    public void validatePseudoHeadersShouldThrowWhenPathHeaderIsEmpty() throws Http2Exception {
+        expectedException.expect(StreamException.class);
+        expectedException.expectMessage("[:path] header cannot be empty.");
+
+        final Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders();
+        headers.set(PATH.value(), "");
+        validateRequestPseudoHeaders(headers, STREAM_ID);
+    }
+
+    @Test
+    public void validatePseudoHeadersShouldThrowWhenSchemeHeaderIsMissing() throws Http2Exception {
+        expectedException.expect(StreamException.class);
+        expectedException.expectMessage("Mandatory header [:scheme] is missing.");
+
+        final Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders();
+        headers.remove(SCHEME.value());
+        validateRequestPseudoHeaders(headers, STREAM_ID);
+    }
+
+    @Test
+    public void validatePseudoHeadersShouldThrowIfMethodHeaderIsNotUnique() throws Http2Exception {
+        expectedException.expect(StreamException.class);
+        expectedException.expectMessage("Header [:method] should have a unique value.");
+
+        final Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders();
+        headers.add(METHOD.value(), "GET");
+        validateRequestPseudoHeaders(headers, STREAM_ID);
+    }
+
+    @Test
+    public void validatePseudoHeadersShouldThrowIfPathHeaderIsNotUnique() throws Http2Exception {
+        expectedException.expect(StreamException.class);
+        expectedException.expectMessage("Header [:path] should have a unique value.");
+
+        final Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders();
+        headers.add(PATH.value(), "/");
+        validateRequestPseudoHeaders(headers, STREAM_ID);
+    }
+
+    @Test
+    public void validatePseudoHeadersShouldThrowIfSchemeHeaderIsNotUnique() throws Http2Exception {
+        expectedException.expect(StreamException.class);
+        expectedException.expectMessage("Header [:scheme] should have a unique value.");
+
+        final Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders();
+        headers.add(SCHEME.value(), "/");
+        validateRequestPseudoHeaders(headers, STREAM_ID);
+    }
+
+    @Test
+    public void validatePseudoHeadersShouldThrowIfMethodHeaderIsNotUniqueWhenMethodIsConnect() throws Http2Exception {
+        expectedException.expect(StreamException.class);
+        expectedException.expectMessage("Header [:method] should have a unique value.");
+
+        final Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders();
+        headers.remove(SCHEME.value());
+        headers.remove(PATH.value());
+        headers.set(METHOD.value(), "CONNECT");
+        headers.add(METHOD.value(), "CONNECT");
+        validateRequestPseudoHeaders(headers, STREAM_ID);
+    }
+
+    @Test
+    public void validatePseudoHeadersShouldThrowIfPathHeaderIsPresentWhenMethodIsConnect() throws Http2Exception {
+        expectedException.expect(StreamException.class);
+        expectedException.expectMessage("Header [:path] must be omitted when using CONNECT method.");
+
+        final Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders();
+        headers.set(METHOD.value(), "CONNECT");
+        headers.remove(SCHEME.value());
+        validateRequestPseudoHeaders(headers, STREAM_ID);
+    }
+
+    @Test
+    public void validatePseudoHeadersShouldThrowIfSchemeHeaderIsPresentWhenMethodIsConnect() throws Http2Exception {
+        expectedException.expect(StreamException.class);
+        expectedException.expectMessage("Header [:scheme] must be omitted when using CONNECT method.");
+
+        final Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders();
+        headers.set(METHOD.value(), "CONNECT");
+        headers.remove(PATH.value());
+        validateRequestPseudoHeaders(headers, STREAM_ID);
+    }
+
+    @Test
+    public void validatePseudoHeadersShouldThrowIfResponseHeaderInRequest() throws Http2Exception {
+        expectedException.expect(StreamException.class);
+        expectedException.expectMessage("Response pseudo-header [:status] is not allowed in a request.");
+
+        final Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders();
+        headers.add(STATUS.value(), "200");
+        validateRequestPseudoHeaders(headers, STREAM_ID);
+    }
+
+}
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexCodecBuilderTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexCodecBuilderTest.java
index c12f5f8b7cc..17a548f5c31 100644
--- a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexCodecBuilderTest.java
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexCodecBuilderTest.java
@@ -42,6 +42,7 @@
 import java.util.concurrent.CountDownLatch;
 
 import static io.netty.handler.codec.http2.Http2CodecUtil.isStreamIdValid;
+import static io.netty.handler.codec.http2.Http2TestUtil.newHttp2HeadersWithRequestPseudoHeaders;
 import static java.util.concurrent.TimeUnit.SECONDS;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -157,8 +158,8 @@ public void multipleOutboundStreams() throws Exception {
         assertTrue(childChannel2.isActive());
         assertFalse(isStreamIdValid(childChannel2.stream().id()));
 
-        Http2Headers headers1 = new DefaultHttp2Headers();
-        Http2Headers headers2 = new DefaultHttp2Headers();
+        Http2Headers headers1 = newHttp2HeadersWithRequestPseudoHeaders();
+        Http2Headers headers2 = newHttp2HeadersWithRequestPseudoHeaders();
         // Test that streams can be made active (headers sent) in different order than the corresponding channels
         // have been created.
         childChannel2.writeAndFlush(new DefaultHttp2HeadersFrame(headers2));
@@ -187,7 +188,7 @@ public void createOutboundStream() throws Exception {
         assertTrue(childChannel.isRegistered());
         assertTrue(childChannel.isActive());
 
-        Http2Headers headers = new DefaultHttp2Headers();
+        Http2Headers headers = newHttp2HeadersWithRequestPseudoHeaders();
         childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(headers));
         ByteBuf data = Unpooled.buffer(100).writeZero(100);
         childChannel.writeAndFlush(new DefaultHttp2DataFrame(data, true));
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2TestUtil.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2TestUtil.java
index 6fa3449709a..a8ba26ba7fa 100644
--- a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2TestUtil.java
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2TestUtil.java
@@ -98,6 +98,13 @@ public static byte[] randomBytes(int size) {
         return data;
     }
 
+    public static Http2Headers newHttp2HeadersWithRequestPseudoHeaders() {
+        return new DefaultHttp2Headers(true)
+            .method("GET")
+            .path("/")
+            .scheme("https");
+    }
+
     /**
      * Returns an {@link AsciiString} that wraps a randomly-filled byte array.
      */
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/HttpToHttp2ConnectionHandlerTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/HttpToHttp2ConnectionHandlerTest.java
index 0e4f3e95202..a66ed56f3a3 100644
--- a/codec-http2/src/test/java/io/netty/handler/codec/http2/HttpToHttp2ConnectionHandlerTest.java
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/HttpToHttp2ConnectionHandlerTest.java
@@ -242,8 +242,8 @@ public void testAuthorityFormRequestTargetHandled() throws Exception {
         final HttpHeaders httpHeaders = request.headers();
         httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 5);
         final Http2Headers http2Headers =
-                new DefaultHttp2Headers().method(new AsciiString("CONNECT")).path(new AsciiString("/"))
-                .scheme(new AsciiString("http")).authority(new AsciiString("www.example.com:80"));
+                new DefaultHttp2Headers().method(new AsciiString("CONNECT"))
+                                         .authority(new AsciiString("www.example.com:80"));
 
         ChannelPromise writePromise = newPromise();
         verifyHeadersOnly(http2Headers, writePromise, clientChannel.writeAndFlush(request, writePromise));
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/InboundHttp2ToHttpAdapterTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/InboundHttp2ToHttpAdapterTest.java
index 8fb4ebfc678..6e53d0e489c 100644
--- a/codec-http2/src/test/java/io/netty/handler/codec/http2/InboundHttp2ToHttpAdapterTest.java
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/InboundHttp2ToHttpAdapterTest.java
@@ -43,6 +43,7 @@
 import io.netty.handler.codec.http.HttpResponseStatus;
 import io.netty.handler.codec.http.HttpVersion;
 import io.netty.handler.codec.http2.Http2TestUtil.Http2Runnable;
+import io.netty.handler.codec.http2.HttpConversionUtil.ExtensionHeaderNames;
 import io.netty.util.AsciiString;
 import io.netty.util.CharsetUtil;
 import io.netty.util.concurrent.Future;
@@ -268,8 +269,11 @@ public void clientRequestOneDataFrame() throws Exception {
             httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
             httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length());
             httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16);
-            final Http2Headers http2Headers = new DefaultHttp2Headers().method(new AsciiString("GET")).path(
-                    new AsciiString("/some/path/resource2"));
+            httpHeaders.set(ExtensionHeaderNames.SCHEME.text(), "http");
+            final Http2Headers http2Headers = new DefaultHttp2Headers()
+                    .method(new AsciiString("GET"))
+                    .scheme(new AsciiString("http"))
+                    .path(new AsciiString("/some/path/resource2"));
             runInChannel(clientChannel, new Http2Runnable() {
                 @Override
                 public void run() throws Http2Exception {
@@ -301,8 +305,11 @@ public void clientRequestMultipleDataFrames() throws Exception {
             httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
             httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length());
             httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16);
-            final Http2Headers http2Headers = new DefaultHttp2Headers().method(new AsciiString("GET")).path(
-                    new AsciiString("/some/path/resource2"));
+            httpHeaders.set(ExtensionHeaderNames.SCHEME.text(), "http");
+            final Http2Headers http2Headers = new DefaultHttp2Headers()
+                    .method(new AsciiString("GET"))
+                    .scheme(new AsciiString("http"))
+                    .path(new AsciiString("/some/path/resource2"));
             final int midPoint = text.length() / 2;
             runInChannel(clientChannel, new Http2Runnable() {
                 @Override
@@ -338,8 +345,11 @@ public void clientRequestMultipleEmptyDataFrames() throws Exception {
             httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
             httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length());
             httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16);
-            final Http2Headers http2Headers = new DefaultHttp2Headers().method(new AsciiString("GET")).path(
-                    new AsciiString("/some/path/resource2"));
+            httpHeaders.set(ExtensionHeaderNames.SCHEME.text(), "http");
+            final Http2Headers http2Headers = new DefaultHttp2Headers()
+                    .method(new AsciiString("GET"))
+                    .scheme("http")
+                    .path(new AsciiString("/some/path/resource2"));
             runInChannel(clientChannel, new Http2Runnable() {
                 @Override
                 public void run() throws Http2Exception {
@@ -372,12 +382,15 @@ public void clientRequestTrailingHeaders() throws Exception {
             httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
             httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length());
             httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16);
+            httpHeaders.set(ExtensionHeaderNames.SCHEME.text(), "http");
             HttpHeaders trailingHeaders = request.trailingHeaders();
             trailingHeaders.set(of("Foo"), of("goo"));
             trailingHeaders.set(of("fOo2"), of("goo2"));
             trailingHeaders.add(of("foO2"), of("goo3"));
-            final Http2Headers http2Headers = new DefaultHttp2Headers().method(new AsciiString("GET")).path(
-                    new AsciiString("/some/path/resource2"));
+            final Http2Headers http2Headers = new DefaultHttp2Headers()
+                    .method(new AsciiString("GET"))
+                    .scheme(new AsciiString("http"))
+                    .path(new AsciiString("/some/path/resource2"));
             final Http2Headers http2Headers2 = new DefaultHttp2Headers()
                     .set(new AsciiString("foo"), new AsciiString("goo"))
                     .set(new AsciiString("foo2"), new AsciiString("goo2"))
@@ -418,15 +431,21 @@ public void clientRequestStreamDependencyInHttpMessageFlow() throws Exception {
             httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
             httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length());
             httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16);
+            httpHeaders.set(ExtensionHeaderNames.SCHEME.text(), "http");
             HttpHeaders httpHeaders2 = request2.headers();
             httpHeaders2.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 5);
             httpHeaders2.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_DEPENDENCY_ID.text(), 3);
             httpHeaders2.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 123);
             httpHeaders2.setInt(HttpHeaderNames.CONTENT_LENGTH, text2.length());
-            final Http2Headers http2Headers = new DefaultHttp2Headers().method(new AsciiString("PUT")).path(
-                    new AsciiString("/some/path/resource"));
-            final Http2Headers http2Headers2 = new DefaultHttp2Headers().method(new AsciiString("PUT")).path(
-                    new AsciiString("/some/path/resource2"));
+            httpHeaders2.set(ExtensionHeaderNames.SCHEME.text(), "http");
+            final Http2Headers http2Headers = new DefaultHttp2Headers()
+                    .method(new AsciiString("PUT"))
+                    .scheme(new AsciiString("http"))
+                    .path(new AsciiString("/some/path/resource"));
+            final Http2Headers http2Headers2 = new DefaultHttp2Headers()
+                    .method(new AsciiString("PUT"))
+                    .scheme(new AsciiString("http"))
+                    .path(new AsciiString("/some/path/resource2"));
             runInChannel(clientChannel, new Http2Runnable() {
                 @Override
                 public void run() throws Http2Exception {
@@ -482,7 +501,10 @@ public void serverRequestPushPromise() throws Exception {
             httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
             httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, 0);
             httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16);
-            final Http2Headers http2Headers3 = new DefaultHttp2Headers().method(new AsciiString("GET"))
+            httpHeaders.set(ExtensionHeaderNames.SCHEME.text(), "http");
+            final Http2Headers http2Headers3 = new DefaultHttp2Headers()
+                    .method(new AsciiString("GET"))
+                    .scheme("http")
                     .path(new AsciiString("/push/test"));
             runInChannel(clientChannel, new Http2Runnable() {
                 @Override
@@ -540,9 +562,11 @@ public void serverResponseHeaderInformational() throws Exception {
         httpHeaders.set(HttpHeaderNames.EXPECT, HttpHeaderValues.CONTINUE);
         httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, 0);
         httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16);
+        httpHeaders.set(ExtensionHeaderNames.SCHEME.text(), "http");
 
         final Http2Headers http2Headers = new DefaultHttp2Headers().method(new AsciiString("PUT"))
                 .path(new AsciiString("/info/test"))
+                .scheme(new AsciiString("http"))
                 .set(new AsciiString(HttpHeaderNames.EXPECT.toString()),
                      new AsciiString(HttpHeaderValues.CONTINUE.toString()));
         final FullHttpMessage response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
diff --git a/testsuite-http2/pom.xml b/testsuite-http2/pom.xml
index 71c40af60db..1c405150d6a 100644
--- a/testsuite-http2/pom.xml
+++ b/testsuite-http2/pom.xml
@@ -94,15 +94,6 @@
             <excludeSpec>5.1 - half closed (remote): Sends a HEADERS frame</excludeSpec>
             <excludeSpec>5.1 - closed: Sends a HEADERS frame</excludeSpec>
             <excludeSpec>5.1.1 - Sends stream identifier that is numerically smaller than previous</excludeSpec>
-            <excludeSpec>8.1.2.2 - Sends a HEADERS frame that contains the connection-specific header field</excludeSpec>
-            <excludeSpec>8.1.2.2 - Sends a HEADERS frame that contains the TE header field with any value other than "trailers"</excludeSpec>
-            <excludeSpec>8.1.2.3 - Sends a HEADERS frame with empty ":path" pseudo-header field</excludeSpec>
-            <excludeSpec>8.1.2.3 - Sends a HEADERS frame that omits ":method" pseudo-header field</excludeSpec>
-            <excludeSpec>8.1.2.3 - Sends a HEADERS frame that omits ":scheme" pseudo-header field</excludeSpec>
-            <excludeSpec>8.1.2.3 - Sends a HEADERS frame that omits ":path" pseudo-header field</excludeSpec>
-            <excludeSpec>8.1.2.3 - Sends a HEADERS frame with duplicated ":method" pseudo-header field</excludeSpec>
-            <excludeSpec>8.1.2.3 - Sends a HEADERS frame with duplicated ":method" pseudo-header field</excludeSpec>
-            <excludeSpec>8.1.2.3 - Sends a HEADERS frame with duplicated ":scheme" pseudo-header field</excludeSpec>
             <excludeSpec>8.1.2.6 - Sends a HEADERS frame with the "content-length" header field which does not equal the DATA frame payload length</excludeSpec>
             <excludeSpec>8.1.2.6 - Sends a HEADERS frame with the "content-length" header field which does not equal the sum of the multiple DATA frames payload length</excludeSpec>
           </excludeSpecs>