Repository: apple/swift-nio-http2
Pull request #16 (2018-08-16 21:03:10)

More Swift-y types for HTTP/2 frame, flags, and settings.
Motivation:

Prior to bringing in my HTTP/2 encoder/decoder, I think my implementations of HTTP/2 frame flags as `OptionSet`s and settings as an enum (i.e. existentials) make for a nicer API, and provide somewhat more information for the compiler to work with when optimizing.

Modifications:

- `HTTP2Frame.FrameFlags` is now an `OptionSet`, and includes the CACHE_DIGEST `reset` and `complete` flag values.
- `HTTP2Frame.FramePayload` now supports all current & proposed HTTP/2 frame types, can return their on-wire identifier, and can return the set of `HTTP2Frame.FrameFlags` that the frame allows.
- `HTTP2Setting` is now an enum with associated values, and includes the SETTINGS_ACCEPT_CACHE_DIGEST and SETTINGS_ENABLE_CONNECT_PROTOCOL values.
- `HTTP2Setting` already includes code to encode/decode itself, though it's not yet used.
- Added `NIOHTTP2Errors.InvalidSettings` error, used when bad settings are encountered by decoder.
- NIOHTTP2 now depends on NIOHPACK.
- `HTTP2Stream` now maintains a `HPACKEncoder` and `HPACKDecoder`.
- Updated `NGHTTP2Session.swift` to handle (i.e. `fatalError("not implemented")`) the additional frame types CONTINUATION, BLOCKED, ORIGIN, and CACHE_DIGEST which were added to `HTTP2Frame.FramePayload`.
- Updated tests to use the new `HTTP2Setting` type.

Result:

Not a lot changes, beyond the in-memory layout of some things. All these types still have nghttp2 conversion methods, and all tests still run successfully.

AlanQuatermain: I've more code to come focussed on the actual encode/decode process, but @Lukasa suggested I make smaller pull requests 😉 

In here I'm just altering some types to be more swift-y, since once the nghttp2 code is gone we'll be working with them directly much more often thus a nicer API is beneficial. Once this is approved I'll be able to start bringing in my encode/decode flows, part of which you can see in the `HTTP2Setting` implementation.

One issue I'm running into, though, has to do with the placement of the HPACK encoders/decoders in the `HTTPStream` type. It looks as though I'd need to take the `StreamManager` (or something like it) from `NGHTTP2Session.swift` and bring it into `HTTP2Parser.swift`, and implement the stream-location and decoding directly within that type. I'm not clear on the behavior of things like the ReentrancyManager though, for instance whether it would be necessary at all once we move away from NGHTTP2. I'm leaning towards renaming `HTTP2Parser` as `NGHTTP2Parser` and implementing a fresh one, with the ability to configure either of them into the channel handler stack. I'd appreciate some discussion around the best way to handle this ahead of time, to try and avoid some of the churn that occurred on #10.
Lukasa: > One issue I'm running into, though, has to do with the placement of the HPACK encoders/decoders in the HTTPStream type. It looks as though I'd need to take the StreamManager (or something like it) from NGHTTP2Session.swift and bring it into HTTP2Parser.swift, and implement the stream-location and decoding directly within that type. I'm not clear on the behavior of things like the ReentrancyManager though, for instance whether it would be necessary at all once we move away from NGHTTP2. I'm leaning towards renaming HTTP2Parser as NGHTTP2Parser and implementing a fresh one, with the ability to configure either of them into the channel handler stack. I'd appreciate some discussion around the best way to handle this ahead of time, to try and avoid some of the churn that occurred on #10.

Yeah, you shouldn't try to retrofit your HPACK encoders/decoders onto the `HTTP2Parser` as it exists today: that object is very definitely nghttp2 specific. Rather than rename it, create a new one with a temporary bad name (e.g. `SwiftHTTP2Parser` or `BetterHTTP2Parser`) and work on it alongside the current one: we can then re-use the testing for both before we eventually remove the nghttp2-based one and replace it with the Swift one.

Reentrancy manager exists to defend against the fact that nghttp2 uses callbacks to notify us of events. This is rarely a good idea in a parser because it leads to a number of tricky edge-cases:

1. You need to know whether the parser is safe to call re-entrantly. nghttp2 doesn't make this clear, so we assume it is not.
2. You need to ensure that if you call out to code you don't control from these callbacks, that you ensure you will never re-enter in an uncontrolled way.
3. You need to ensure that you don't screw up feeding data into the parser.

All of this is managed by reentrancy manager essentially turning all recursive re-entrant calls into `HTTP2Parser` into serial calls instead. For our new parser, we'll want to avoid this risk altogether by changing the parser to avoid using callbacks and instead using return values to trigger callouts. If we use that design, we do not need the re-entrancy manager.

Patch:
diff --git a/Sources/NIOHTTP2/HTTP2Frame.swift b/Sources/NIOHTTP2/HTTP2Frame.swift
index a7c4dfb..5731395 100644
--- a/Sources/NIOHTTP2/HTTP2Frame.swift
+++ b/Sources/NIOHTTP2/HTTP2Frame.swift
@@ -21,108 +21,174 @@ public struct HTTP2Frame {
     /// The payload of this HTTP/2 frame.
     public var payload: FramePayload
 
-    /// The frame flags as an 8-bit integer. To set/unset well-defined flags, consider using the
-    /// other properties on this object (e.g. `endStream`).
-    public var flags: UInt8
-
+    /// The frame flags.
+    public var flags: FrameFlags
+    
     /// The frame stream ID as a 32-bit integer.
     public var streamID: HTTP2StreamID
 
-    // Whether the END_STREAM flag bit is set.
-    public var endStream: Bool {
-        get {
-            switch self.payload {
-            case .data, .headers:
-                return (self.flags & UInt8(NGHTTP2_FLAG_END_STREAM.rawValue) != 0)
-            default:
-                return false
-            }
-        }
-        set {
-            switch self.payload {
-            case .data, .headers:
-                self.flags |= UInt8(NGHTTP2_FLAG_END_STREAM.rawValue)
-            default:
-                break
-            }
-        }
-    }
-
-    // Whether the PADDED flag bit is set.
-    public var padded: Bool {
-        get {
-            switch self.payload {
-            case .data, .headers, .pushPromise:
-                return (self.flags & UInt8(NGHTTP2_FLAG_PADDED.rawValue) != 0)
-            default:
-                return false
-            }
-        }
-        set {
-            switch self.payload {
-            case .data, .headers, .pushPromise:
-                self.flags |= UInt8(NGHTTP2_FLAG_PADDED.rawValue)
-            default:
-                break
-            }
-        }
-    }
-
-    // Whether the PRIORITY flag bit is set.
-    public var priority: Bool {
-        get {
-            if case .headers = self.payload {
-                 return (self.flags & UInt8(NGHTTP2_FLAG_PRIORITY.rawValue) != 0)
-            } else {
-                return false
-            }
-        }
-        set {
-            if case .headers = self.payload {
-                self.flags |= UInt8(NGHTTP2_FLAG_PRIORITY.rawValue)
-            }
-        }
-    }
-
-    // Whether the ACK flag bit is set.
-    public var ack: Bool {
-        get {
-            switch self.payload {
-            case .settings, .ping:
-                return (self.flags & UInt8(NGHTTP2_FLAG_ACK.rawValue) != 0)
-            default:
-                return false
-            }
-        }
-        set {
-            switch self.payload {
-            case .settings, .ping:
-                self.flags |= UInt8(NGHTTP2_FLAG_ACK.rawValue)
-            default:
-                break
-            }
-        }
-    }
-
+    /// Frame-type-specific payload data.
     public enum FramePayload {
+        /// A DATA frame, containing raw bytes.
+        ///
+        /// See [RFC 7540 § 6.1](https://httpwg.org/specs/rfc7540.html#rfc.section.6.1).
         case data(IOData)
+        
+        /// A HEADERS frame, containing all headers or trailers associated with a request
+        /// or response.
+        ///
+        /// Note that swift-nio-http2 automatically coalesces HEADERS and CONTINUATION
+        /// frames into a single `FramePayload.headers` instance.
+        ///
+        /// See [RFC 7540 § 6.2](https://httpwg.org/specs/rfc7540.html#rfc.section.6.2).
         case headers(HTTPHeaders)
+        
+        /// A PRIORITY frame, used to change priority and dependency ordering among
+        /// streams.
+        ///
+        /// See [RFC 7540 § 6.3](https://httpwg.org/specs/rfc7540.html#rfc.section.6.3).
         case priority
+        
+        /// A RST_STREAM (reset stream) frame, sent when a stream has encountered an error
+        /// condition and needs to be terminated as a result.
+        ///
+        /// See [RFC 7540 § 6.4](https://httpwg.org/specs/rfc7540.html#rfc.section.6.4).
         case rstStream(HTTP2ErrorCode)
+        
+        /// A SETTINGS frame, containing various connection--level settings and their
+        /// desired values.
+        ///
+        /// See [RFC 7540 § 6.5](https://httpwg.org/specs/rfc7540.html#rfc.section.6.5).
         case settings([HTTP2Setting])
+        
+        /// A PUSH_PROMISE frame, used to notify a peer in advance of streams that a sender
+        /// intends to initiate. It performs much like a request's HEADERS frame, informing
+        /// a peer that the response for a theoretical request like the one in the promise
+        /// will arrive on a new stream.
+        ///
+        /// As with the HEADERS frame, swift-nio-http2 will coalesce an initial PUSH_PROMISE
+        /// frame with any CONTINUATION frames that follow, emitting a single
+        /// `FramePayload.pushPromise` instance for the complete set.
+        ///
+        /// See [RFC 7540 § 6.6](https://httpwg.org/specs/rfc7540.html#rfc.section.6.6).
+        ///
+        /// For more information on server push in HTTP/2, see
+        /// [RFC 7540 § 8.2](https://httpwg.org/specs/rfc7540.html#rfc.section.8.2).
         case pushPromise
+        
+        /// A PING frame, used to measure round-trip time between endpoints.
+        ///
+        /// See [RFC 7540 § 6.7](https://httpwg.org/specs/rfc7540.html#rfc.section.6.7).
         case ping(HTTP2PingData)
+        
+        /// A GOAWAY frame, used to request that a peer immediately cease communication with
+        /// the sender. It contains a stream ID indicating the last stream that will be processed
+        /// by the sender, an error code (if the shutdown was caused by an error), and optionally
+        /// some additional diagnostic data.
+        ///
+        /// See [RFC 7540 § 6.8](https://httpwg.org/specs/rfc7540.html#rfc.section.6.8).
         case goAway(lastStreamID: HTTP2StreamID, errorCode: HTTP2ErrorCode, opaqueData: ByteBuffer?)
+        
+        /// A WINDOW_UPDATE frame. This is used to implement flow control of DATA frames,
+        /// allowing peers to advertise and update the amount of data they are prepared to
+        /// process at any given moment.
+        ///
+        /// See [RFC 7540 § 6.9](https://httpwg.org/specs/rfc7540.html#rfc.section.6.9).
         case windowUpdate(windowSizeIncrement: Int)
-        case alternativeService
+        
+        /// An ALTSVC frame. This is sent by an HTTP server to indicate alternative origin
+        /// locations for accessing the same resource, for instance via another protocol,
+        /// or over TLS. It consists of an origin and a list of alternate protocols and
+        /// the locations at which they may be addressed.
+        ///
+        /// See [RFC 7838 § 4](https://tools.ietf.org/html/rfc7838#section-4).
+        case alternativeService(origin: String?, field: ByteBuffer?)
+        
+        /// An ORIGIN frame. This allows servers which allow access to multiple origins
+        /// via the same socket connection to identify which origins may be accessed in
+        /// this manner.
+        ///
+        /// See [RFC 8336 § 2](https://tools.ietf.org/html/rfc8336#section-2).
+        case origin([String])
+        
+        /// The one-byte identifier used to indicate the type of a frame on the wire.
+        var code: UInt8 {
+            switch self {
+            case .data:                 return 0x0
+            case .headers:              return 0x1
+            case .priority:             return 0x2
+            case .rstStream:            return 0x3
+            case .settings:             return 0x4
+            case .pushPromise:          return 0x5
+            case .ping:                 return 0x6
+            case .goAway:               return 0x7
+            case .windowUpdate:         return 0x8
+            case .alternativeService:   return 0xa
+            case .origin:               return 0xc
+            }
+        }
+        
+        /// The set of flags that are permitted in the flags field of a particular frame.
+        var allowedFlags: FrameFlags {
+            switch self {
+            case .data:
+                return [.padded, .endStream]
+            case .headers:
+                return [.endStream, .endHeaders, .padded, .priority]
+            case .pushPromise:
+                return [.endHeaders, .padded]
+            case .settings, .ping:
+                return .ack
+            case .priority, .rstStream, .goAway, .windowUpdate,
+                 .alternativeService, .origin:
+                return []
+            }
+        }
+    }
+    
+    /// The flags supported by the frame types understood by this protocol.
+    public struct FrameFlags: OptionSet {
+        public typealias RawValue = UInt8
+        
+        public private(set) var rawValue: UInt8
+        
+        public init(rawValue: UInt8) {
+            self.rawValue = rawValue
+        }
+        
+        /// END_STREAM flag. Valid on DATA and HEADERS frames.
+        public static let endStream     = FrameFlags(rawValue: 0x01)
+        
+        /// ACK flag. Valid on SETTINGS and PING frames.
+        public static let ack           = FrameFlags(rawValue: 0x01)
+        
+        /// END_HEADERS flag. Valid on HEADERS, CONTINUATION, and PUSH_PROMISE frames.
+        public static let endHeaders    = FrameFlags(rawValue: 0x04)
+        
+        /// PADDED flag. Valid on DATA, HEADERS, CONTINUATION, and PUSH_PROMISE frames.
+        ///
+        /// NB: swift-nio-http2 does not automatically pad outgoing frames.
+        public static let padded        = FrameFlags(rawValue: 0x08)
+        
+        /// PRIORITY flag. Valid on HEADERS frames, specifically as the first frame sent
+        /// on a new stream.
+        public static let priority      = FrameFlags(rawValue: 0x20)
+        
+        // useful for test cases
+        internal static var allFlags: FrameFlags = [.endStream, .endHeaders, .padded, .priority]
     }
 }
 
 
 internal extension HTTP2Frame {
+    internal init(streamID: HTTP2StreamID, flags: HTTP2Frame.FrameFlags, payload: HTTP2Frame.FramePayload) {
+        self.streamID = streamID
+        self.flags = flags.intersection(payload.allowedFlags)
+        self.payload = payload
+    }
     internal init(streamID: HTTP2StreamID, flags: UInt8, payload: HTTP2Frame.FramePayload) {
         self.streamID = streamID
-        self.flags = flags
+        self.flags = FrameFlags(rawValue: flags).intersection(payload.allowedFlags)
         self.payload = payload
     }
 }
@@ -131,7 +197,7 @@ public extension HTTP2Frame {
     /// Constructs a frame header for a given stream ID. All flags are unset.
     public init(streamID: HTTP2StreamID, payload: HTTP2Frame.FramePayload) {
         self.streamID = streamID
-        self.flags = 0
+        self.flags = []
         self.payload = payload
     }
 }
diff --git a/Sources/NIOHTTP2/HTTP2Settings.swift b/Sources/NIOHTTP2/HTTP2Settings.swift
index 8ed3e71..afac85c 100644
--- a/Sources/NIOHTTP2/HTTP2Settings.swift
+++ b/Sources/NIOHTTP2/HTTP2Settings.swift
@@ -52,6 +52,9 @@ public struct HTTP2SettingsParameter {
 
     /// Corresponds to SETTINGS_MAX_HEADER_LIST_SIZE
     public static let maxHeaderListSize = HTTP2SettingsParameter(6)
+    
+    /// Corresponds to SETTINGS_ENABLE_CONNECT_PROTOCOL from RFC 8441.
+    public static let enableConnectProtocol = HTTP2SettingsParameter(8)
 }
 
 extension HTTP2SettingsParameter: Equatable {
diff --git a/Sources/NIOHTTP2/HTTP2ToHTTP1Codec.swift b/Sources/NIOHTTP2/HTTP2ToHTTP1Codec.swift
index 0715fea..e19f40c 100644
--- a/Sources/NIOHTTP2/HTTP2ToHTTP1Codec.swift
+++ b/Sources/NIOHTTP2/HTTP2ToHTTP1Codec.swift
@@ -94,13 +94,13 @@ public final class HTTP2ToHTTP1ClientCodec: ChannelInboundHandler, ChannelOutbou
             } else {
                 let respHead = HTTPResponseHead(http2HeaderBlock: headers)
                 ctx.fireChannelRead(self.wrapInboundOut(.head(respHead)))
-                if frame.endStream {
+                if frame.flags.contains(.endStream) {
                     ctx.fireChannelRead(self.wrapInboundOut(.end(nil)))
                 }
             }
         case .data(.byteBuffer(let b)):
             ctx.fireChannelRead(self.wrapInboundOut(.body(b)))
-            if frame.endStream {
+            if frame.flags.contains(.endStream) {
                 ctx.fireChannelRead(self.wrapInboundOut(.end(nil)))
             }
         case .alternativeService, .rstStream, .priority, .windowUpdate:
@@ -131,7 +131,7 @@ public final class HTTP2ToHTTP1ClientCodec: ChannelInboundHandler, ChannelOutbou
             }
 
             var frame = HTTP2Frame(streamID: self.streamID, payload: payload)
-            frame.endStream = true
+            frame.flags.insert(.endStream)
             ctx.write(self.wrapOutboundOut(frame), promise: promise)
         }
     }
@@ -170,13 +170,13 @@ public final class HTTP2ToHTTP1ServerCodec: ChannelInboundHandler, ChannelOutbou
             } else {
                 let reqHead = HTTPRequestHead(http2HeaderBlock: headers)
                 ctx.fireChannelRead(self.wrapInboundOut(.head(reqHead)))
-                if frame.endStream {
+                if frame.flags.contains(.endStream) {
                     ctx.fireChannelRead(self.wrapInboundOut(.end(nil)))
                 }
             }
         case .data(.byteBuffer(let b)):
             ctx.fireChannelRead(self.wrapInboundOut(.body(b)))
-            if frame.endStream {
+            if frame.flags.contains(.endStream) {
                 ctx.fireChannelRead(self.wrapInboundOut(.end(nil)))
             }
         case .alternativeService, .rstStream, .priority, .windowUpdate:
@@ -207,7 +207,7 @@ public final class HTTP2ToHTTP1ServerCodec: ChannelInboundHandler, ChannelOutbou
             }
 
             var frame = HTTP2Frame(streamID: self.streamID, payload: payload)
-            frame.endStream = true
+            frame.flags.insert(.endStream)
             ctx.write(self.wrapOutboundOut(frame), promise: promise)
         }
     }
diff --git a/Sources/NIOHTTP2/NGHTTP2Session.swift b/Sources/NIOHTTP2/NGHTTP2Session.swift
index 0e7436e..54469d1 100644
--- a/Sources/NIOHTTP2/NGHTTP2Session.swift
+++ b/Sources/NIOHTTP2/NGHTTP2Session.swift
@@ -628,6 +628,8 @@ class NGHTTP2Session {
             fatalError("not implemented")
         case .alternativeService:
             fatalError("not implemented")
+        case .origin(_):
+            fatalError("not implemented")
         }
     }
 
@@ -675,7 +677,7 @@ class NGHTTP2Session {
             preconditionFailure("Attempting to send non-headers frame")
         }
 
-        let isEndStream = frame.endStream
+        let isEndStream = frame.flags.contains(.endStream)
         let flags = isEndStream ? UInt8(NGHTTP2_FLAG_END_STREAM.rawValue) : UInt8(0)
 
         guard let networkStreamID = frame.streamID.networkStreamID else {
@@ -742,7 +744,7 @@ class NGHTTP2Session {
         streamState.dataProvider.bufferWrite(write: data, promise: promise)
 
         // If this has END_STREAM set, we do not expect trailers.
-        if frame.endStream {
+        if frame.flags.contains(.endStream) {
             streamState.dataProvider.bufferEOF(trailers: nil)
         }
 


Test patch:
diff --git a/Tests/NIOHTTP2Tests/HTTP2StreamMultiplexerTests.swift b/Tests/NIOHTTP2Tests/HTTP2StreamMultiplexerTests.swift
index 74c3a1c..fedc3eb 100644
--- a/Tests/NIOHTTP2Tests/HTTP2StreamMultiplexerTests.swift
+++ b/Tests/NIOHTTP2Tests/HTTP2StreamMultiplexerTests.swift
@@ -179,7 +179,7 @@ final class HTTP2StreamMultiplexerTests: XCTestCase {
         let streamIDs = stride(from: 1, to: 100, by: 2).map { HTTP2StreamID(knownID: Int32($0)) }
         for streamID in streamIDs {
             var frame = HTTP2Frame(streamID: streamID, payload: .headers(HTTPHeaders()))
-            frame.endStream = true
+            frame.flags.insert(.endStream)
             XCTAssertNoThrow(try self.channel.writeInbound(frame))
         }
         XCTAssertEqual(completedChannelCount, 0)
@@ -202,7 +202,7 @@ final class HTTP2StreamMultiplexerTests: XCTestCase {
         // First, set up the frames we want to send/receive.
         let streamID = HTTP2StreamID(knownID: Int32(1))
         var frame = HTTP2Frame(streamID: streamID, payload: .headers(HTTPHeaders()))
-        frame.endStream = true
+        frame.flags.insert(.endStream)
         let rstStreamFrame = HTTP2Frame(streamID: streamID, payload: .rstStream(.cancel))
 
         let multiplexer = HTTP2StreamMultiplexer { (channel, _) in
@@ -236,7 +236,7 @@ final class HTTP2StreamMultiplexerTests: XCTestCase {
         // First, set up the frames we want to send/receive.
         let streamID = HTTP2StreamID(knownID: Int32(1))
         var frame = HTTP2Frame(streamID: streamID, payload: .headers(HTTPHeaders()))
-        frame.endStream = true
+        frame.flags.insert(.endStream)
         let goAwayFrame = HTTP2Frame(streamID: .rootStream, payload: .goAway(lastStreamID: .rootStream, errorCode: .http11Required, opaqueData: nil))
 
         let multiplexer = HTTP2StreamMultiplexer { (channel, _) in
@@ -613,7 +613,7 @@ final class HTTP2StreamMultiplexerTests: XCTestCase {
         let secondStreamID = HTTP2StreamID(knownID: 3)
         for streamID in [firstStreamID, secondStreamID] {
             var frame = HTTP2Frame(streamID: streamID, payload: .headers(HTTPHeaders()))
-            frame.endStream = true
+            frame.flags.insert(.endStream)
             XCTAssertNoThrow(try self.channel.writeInbound(frame))
         }
         XCTAssertEqual(channels.count, 2)
@@ -649,7 +649,7 @@ final class HTTP2StreamMultiplexerTests: XCTestCase {
         // Let's open a stream.
         let streamID = HTTP2StreamID(knownID: 1)
         var frame = HTTP2Frame(streamID: streamID, payload: .headers(HTTPHeaders()))
-        frame.endStream = true
+        frame.flags.insert(.endStream)
         XCTAssertNoThrow(try self.channel.writeInbound(frame))
         XCTAssertNotNil(channel)
 
@@ -680,7 +680,7 @@ final class HTTP2StreamMultiplexerTests: XCTestCase {
         // Let's open a stream.
         let streamID = HTTP2StreamID(knownID: 1)
         var frame = HTTP2Frame(streamID: streamID, payload: .headers(HTTPHeaders()))
-        frame.endStream = true
+        frame.flags.insert(.endStream)
         XCTAssertNoThrow(try self.channel.writeInbound(frame))
         XCTAssertNotNil(channel)
 
@@ -711,7 +711,7 @@ final class HTTP2StreamMultiplexerTests: XCTestCase {
         // Let's open a stream.
         let streamID = HTTP2StreamID(knownID: 1)
         var frame = HTTP2Frame(streamID: streamID, payload: .headers(HTTPHeaders()))
-        frame.endStream = true
+        frame.flags.insert(.endStream)
         XCTAssertNoThrow(try self.channel.writeInbound(frame))
         XCTAssertNotNil(channel)
 
@@ -949,7 +949,7 @@ final class HTTP2StreamMultiplexerTests: XCTestCase {
         // Let's open a stream.
         let streamID = HTTP2StreamID(knownID: 1)
         var frame = HTTP2Frame(streamID: streamID, payload: .headers(HTTPHeaders()))
-        frame.endStream = true
+        frame.flags.insert(.endStream)
         XCTAssertNoThrow(try self.channel.writeInbound(frame))
 
         // No handlerRemoved so far.
@@ -980,7 +980,7 @@ final class HTTP2StreamMultiplexerTests: XCTestCase {
         // Let's open a stream.
         let streamID = HTTP2StreamID(knownID: 1)
         var frame = HTTP2Frame(streamID: streamID, payload: .headers(HTTPHeaders()))
-        frame.endStream = true
+        frame.flags.insert(.endStream)
         XCTAssertNoThrow(try self.channel.writeInbound(frame))
 
         // No handlerRemoved so far.
diff --git a/Tests/NIOHTTP2Tests/HTTP2ToHTTP1CodecTests.swift b/Tests/NIOHTTP2Tests/HTTP2ToHTTP1CodecTests.swift
index 55bfc39..550fddf 100644
--- a/Tests/NIOHTTP2Tests/HTTP2ToHTTP1CodecTests.swift
+++ b/Tests/NIOHTTP2Tests/HTTP2ToHTTP1CodecTests.swift
@@ -93,7 +93,7 @@ final class HTTP2ToHTTP1CodecTests: XCTestCase {
         var bodyData = self.channel.allocator.buffer(capacity: 12)
         bodyData.write(staticString: "hello, world!")
         var dataFrame = HTTP2Frame(streamID: streamID, payload: .data(.byteBuffer(bodyData)))
-        dataFrame.endStream = true
+        dataFrame.flags.insert(.endStream)
         XCTAssertNoThrow(try self.channel.writeInbound(dataFrame))
         self.channel.assertReceivedServerRequestPart(.body(bodyData))
         self.channel.assertReceivedServerRequestPart(.end(nil))
@@ -108,7 +108,7 @@ final class HTTP2ToHTTP1CodecTests: XCTestCase {
         // A basic request.
         let requestHeaders = HTTPHeaders([(":path", "/get"), (":method", "GET"), (":scheme", "https"), (":authority", "example.org"), ("other", "header")])
         var headersFrame = HTTP2Frame(streamID: streamID, payload: .headers(requestHeaders))
-        headersFrame.endStream = true
+        headersFrame.flags.insert(.endStream)
         XCTAssertNoThrow(try self.channel.writeInbound(headersFrame))
 
         var expectedRequestHead = HTTPRequestHead(version: HTTPVersion(major: 2, minor: 0), method: .GET, uri: "/get")
@@ -137,7 +137,7 @@ final class HTTP2ToHTTP1CodecTests: XCTestCase {
         // Ok, we're going to send trailers.
         let trailers = HTTPHeaders([("a trailer", "yes"), ("another trailer", "also yes")])
         var trailersFrame = HTTP2Frame(streamID: streamID, payload: .headers(trailers))
-        trailersFrame.endStream = true
+        trailersFrame.flags.insert(.endStream)
         XCTAssertNoThrow(try self.channel.writeInbound(trailersFrame))
 
         self.channel.assertReceivedServerRequestPart(.end(trailers))
@@ -280,7 +280,7 @@ final class HTTP2ToHTTP1CodecTests: XCTestCase {
         var bodyData = self.channel.allocator.buffer(capacity: 12)
         bodyData.write(staticString: "hello, world!")
         var dataFrame = HTTP2Frame(streamID: streamID, payload: .data(.byteBuffer(bodyData)))
-        dataFrame.endStream = true
+        dataFrame.flags.insert(.endStream)
         XCTAssertNoThrow(try self.channel.writeInbound(dataFrame))
         self.channel.assertReceivedClientResponsePart(.body(bodyData))
         self.channel.assertReceivedClientResponsePart(.end(nil))
@@ -295,7 +295,7 @@ final class HTTP2ToHTTP1CodecTests: XCTestCase {
         // A basic response.
         let responseHeaders = HTTPHeaders([(":status", "200"), ("other", "header")])
         var headersFrame = HTTP2Frame(streamID: streamID, payload: .headers(responseHeaders))
-        headersFrame.endStream = true
+        headersFrame.flags.insert(.endStream)
         XCTAssertNoThrow(try self.channel.writeInbound(headersFrame))
 
         var expectedResponseHead = HTTPResponseHead(version: .init(major: 2, minor: 0), status: .ok)
@@ -322,7 +322,7 @@ final class HTTP2ToHTTP1CodecTests: XCTestCase {
         // Ok, we're going to send trailers.
         let trailers = HTTPHeaders([("a trailer", "yes"), ("another trailer", "also yes")])
         var trailersFrame = HTTP2Frame(streamID: streamID, payload: .headers(trailers))
-        trailersFrame.endStream = true
+        trailersFrame.flags.insert(.endStream)
         XCTAssertNoThrow(try self.channel.writeInbound(trailersFrame))
 
         self.channel.assertReceivedClientResponsePart(.end(trailers))
@@ -406,7 +406,7 @@ final class HTTP2ToHTTP1CodecTests: XCTestCase {
         // Now a response.
         let responseHeaders = HTTPHeaders([(":status", "200"), ("other", "header")])
         var responseFrame = HTTP2Frame(streamID: streamID, payload: .headers(responseHeaders))
-        responseFrame.endStream = true
+        responseFrame.flags.insert(.endStream)
         XCTAssertNoThrow(try self.channel.writeInbound(responseFrame))
 
         var expectedResponseHead = HTTPResponseHead(version: .init(major: 2, minor: 0), status: .ok)
diff --git a/Tests/NIOHTTP2Tests/SimpleClientServerTests.swift b/Tests/NIOHTTP2Tests/SimpleClientServerTests.swift
index fe830a9..5b61833 100644
--- a/Tests/NIOHTTP2Tests/SimpleClientServerTests.swift
+++ b/Tests/NIOHTTP2Tests/SimpleClientServerTests.swift
@@ -201,14 +201,14 @@ class SimpleClientServerTests: XCTestCase {
         let clientStreamID = HTTP2StreamID()
         let reqFrame = HTTP2Frame(streamID: clientStreamID, payload: .headers(headers))
         var reqBodyFrame = HTTP2Frame(streamID: clientStreamID, payload: .data(.byteBuffer(requestBody)))
-        reqBodyFrame.endStream = true
+        reqBodyFrame.flags.insert(.endStream)
 
         let serverStreamID = try self.assertFramesRoundTrip(frames: [reqFrame, reqBodyFrame], sender: self.clientChannel, receiver: self.serverChannel).first!.streamID
 
         // Let's send a quick response back.
         let responseHeaders = HTTPHeaders([(":status", "200"), ("content-length", "0")])
         var respFrame = HTTP2Frame(streamID: serverStreamID, payload: .headers(responseHeaders))
-        respFrame.endStream = true
+        respFrame.flags.insert(.endStream)
         try self.assertFramesRoundTrip(frames: [respFrame], sender: self.serverChannel, receiver: self.clientChannel)
 
         XCTAssertNoThrow(try self.clientChannel.finish())
@@ -232,7 +232,7 @@ class SimpleClientServerTests: XCTestCase {
             let streamID = HTTP2StreamID()
             let reqFrame = HTTP2Frame(streamID: streamID, payload: .headers(requestHeaders))
             var reqBodyFrame = HTTP2Frame(streamID: streamID, payload: .data(.byteBuffer(requestBody)))
-            reqBodyFrame.endStream = true
+            reqBodyFrame.flags.insert(.endStream)
 
             self.clientChannel.write(reqFrame, promise: nil)
             self.clientChannel.write(reqBodyFrame, promise: nil)
@@ -303,13 +303,13 @@ class SimpleClientServerTests: XCTestCase {
         var requestBody = self.clientChannel.allocator.buffer(capacity: 128)
         requestBody.write(staticString: "A simple HTTP/2 request.")
         var reqBodyFrame = HTTP2Frame(streamID: clientStreamID, payload: .data(.byteBuffer(requestBody)))
-        reqBodyFrame.endStream = true
+        reqBodyFrame.flags.insert(.endStream)
         try self.assertFramesRoundTrip(frames: [reqBodyFrame], sender: self.clientChannel, receiver: self.serverChannel)
 
         // The server will respond, closing this stream.
         let responseHeaders = HTTPHeaders([(":status", "200"), ("content-length", "0")])
         var respFrame = HTTP2Frame(streamID: serverStreamID, payload: .headers(responseHeaders))
-        respFrame.endStream = true
+        respFrame.flags.insert(.endStream)
         try self.assertFramesRoundTrip(frames: [respFrame], sender: self.serverChannel, receiver: self.clientChannel)
 
         // The server can now GOAWAY down to stream 1. We evaluate the bytes here ourselves becuase the client won't see this frame.
@@ -355,7 +355,7 @@ class SimpleClientServerTests: XCTestCase {
 
         // Now we'll try to send this in a DATA frame.
         var dataFrame = HTTP2Frame(streamID: clientStreamID, payload: .data(.byteBuffer(buffer)))
-        dataFrame.endStream = true
+        dataFrame.flags.insert(.endStream)
         self.clientChannel.writeAndFlush(dataFrame, promise: nil)
         self.interactInMemory(self.clientChannel, self.serverChannel)
 
@@ -372,7 +372,7 @@ class SimpleClientServerTests: XCTestCase {
         // Now send a response from the server and shut things down.
         let responseHeaders = HTTPHeaders([(":status", "200"), ("content-length", "0")])
         var respFrame = HTTP2Frame(streamID: serverStreamID, payload: .headers(responseHeaders))
-        respFrame.endStream = true
+        respFrame.flags.insert(.endStream)
         try self.assertFramesRoundTrip(frames: [respFrame], sender: self.serverChannel, receiver: self.clientChannel)
 
         XCTAssertNoThrow(try self.clientChannel.finish())
@@ -392,14 +392,14 @@ class SimpleClientServerTests: XCTestCase {
             let clientStreamID = HTTP2StreamID()
             let reqFrame = HTTP2Frame(streamID: clientStreamID, payload: .headers(headers))
             var reqBodyFrame = HTTP2Frame(streamID: clientStreamID, payload: .data(.fileRegion(region)))
-            reqBodyFrame.endStream = true
+            reqBodyFrame.flags.insert(.endStream)
 
             let serverStreamID = try self.assertFramesRoundTrip(frames: [reqFrame, reqBodyFrame], sender: self.clientChannel, receiver: self.serverChannel).first!.streamID
 
             // Let's send a quick response back.
             let responseHeaders = HTTPHeaders([(":status", "200"), ("content-length", "0")])
             var respFrame = HTTP2Frame(streamID: serverStreamID, payload: .headers(responseHeaders))
-            respFrame.endStream = true
+            respFrame.flags.insert(.endStream)
             try self.assertFramesRoundTrip(frames: [respFrame], sender: self.serverChannel, receiver: self.clientChannel)
         }
 
@@ -432,7 +432,7 @@ class SimpleClientServerTests: XCTestCase {
 
             // Ok, we're gonna send the body here. This should create 4 streams.
             var reqBodyFrame = HTTP2Frame(streamID: clientStreamID, payload: .data(.fileRegion(region)))
-            reqBodyFrame.endStream = true
+            reqBodyFrame.flags.insert(.endStream)
             self.clientChannel.writeAndFlush(reqBodyFrame, promise: nil)
             self.interactInMemory(self.clientChannel, self.serverChannel)
 
@@ -449,7 +449,7 @@ class SimpleClientServerTests: XCTestCase {
             // Let's send a quick response back.
             let responseHeaders = HTTPHeaders([(":status", "200"), ("content-length", "0")])
             var respFrame = HTTP2Frame(streamID: serverStreamID, payload: .headers(responseHeaders))
-            respFrame.endStream = true
+            respFrame.flags.insert(.endStream)
             try self.assertFramesRoundTrip(frames: [respFrame], sender: self.serverChannel, receiver: self.clientChannel)
 
             // No frames left.
@@ -500,12 +500,12 @@ class SimpleClientServerTests: XCTestCase {
 
         // Let's complete one of the streams by sending data for the first stream on the client, and responding on the server.
         var dataFrame = HTTP2Frame(streamID: clientStreamIDs.first!, payload: .data(.byteBuffer(requestBody)))
-        dataFrame.endStream = true
+        dataFrame.flags.insert(.endStream)
         self.clientChannel.writeAndFlush(dataFrame, promise: nil)
 
         let responseHeaders = HTTPHeaders([(":status", "200"), ("content-length", "0")])
         var respFrame = HTTP2Frame(streamID: serverStreamIDs.first!, payload: .headers(responseHeaders))
-        respFrame.endStream = true
+        respFrame.flags.insert(.endStream)
         self.serverChannel.writeAndFlush(respFrame, promise: nil)
 
         // Now we expect the following things to have happened: 1) the client will have seen the server's response,
@@ -580,7 +580,7 @@ class SimpleClientServerTests: XCTestCase {
         let clientStreamID = HTTP2StreamID()
         let reqFrame = HTTP2Frame(streamID: clientStreamID, payload: .headers(headers))
         var reqBodyFrame = HTTP2Frame(streamID: clientStreamID, payload: .data(.byteBuffer(requestBody)))
-        reqBodyFrame.endStream = true
+        reqBodyFrame.flags.insert(.endStream)
         self.clientChannel.write(reqFrame, promise: nil)
 
         var receivedError: Error? = nil
@@ -610,7 +610,7 @@ class SimpleClientServerTests: XCTestCase {
         let clientStreamID = HTTP2StreamID()
         let reqFrame = HTTP2Frame(streamID: clientStreamID, payload: .headers(headers))
         var reqBodyFrame = HTTP2Frame(streamID: clientStreamID, payload: .data(.byteBuffer(requestBody)))
-        reqBodyFrame.endStream = true
+        reqBodyFrame.flags.insert(.endStream)
         self.clientChannel.write(reqFrame, promise: nil)
 
         var receivedError: Error? = nil
@@ -722,7 +722,7 @@ class SimpleClientServerTests: XCTestCase {
         let clientStreamID = HTTP2StreamID()
         let reqFrame = HTTP2Frame(streamID: clientStreamID, payload: .headers(headers))
         var reqBodyFrame = HTTP2Frame(streamID: clientStreamID, payload: .data(.byteBuffer(requestBody)))
-        reqBodyFrame.endStream = true
+        reqBodyFrame.flags.insert(.endStream)
         self.clientChannel.write(reqFrame, promise: nil)
         self.clientChannel.write(reqBodyFrame, promise: nil)
 
@@ -764,7 +764,7 @@ class SimpleClientServerTests: XCTestCase {
         for _ in 0..<63 {
             self.clientChannel.write(reqBodyFrame, promise: nil)
         }
-        reqBodyFrame.endStream = true
+        reqBodyFrame.flags.insert(.endStream)
         self.clientChannel.writeAndFlush(reqBodyFrame, promise: nil)
 
         // Ok, we now want to send this data to the server.
@@ -991,7 +991,7 @@ class SimpleClientServerTests: XCTestCase {
         let reqFrame = HTTP2Frame(streamID: clientStreamID, payload: .headers(headers))
         let reqBodyFrame = HTTP2Frame(streamID: clientStreamID, payload: .data(.byteBuffer(requestBody)))
         var trailerFrame = HTTP2Frame(streamID: clientStreamID, payload: .headers(trailers))
-        trailerFrame.endStream = true
+        trailerFrame.flags.insert(.endStream)
 
         let serverStreamID = try self.assertFramesRoundTrip(frames: [reqFrame, reqBodyFrame, trailerFrame], sender: self.clientChannel, receiver: self.serverChannel).first!.streamID
 
@@ -999,7 +999,7 @@ class SimpleClientServerTests: XCTestCase {
         let responseHeaders = HTTPHeaders([(":status", "200"), ("content-length", "0")])
         let respFrame = HTTP2Frame(streamID: serverStreamID, payload: .headers(responseHeaders))
         var respTrailersFrame = HTTP2Frame(streamID: serverStreamID, payload: .headers(trailers))
-        respTrailersFrame.endStream = true
+        respTrailersFrame.flags.insert(.endStream)
         try self.assertFramesRoundTrip(frames: [respFrame, respTrailersFrame], sender: self.serverChannel, receiver: self.clientChannel)
 
         XCTAssertNoThrow(try self.clientChannel.finish())
@@ -1014,7 +1014,7 @@ class SimpleClientServerTests: XCTestCase {
         let headers = HTTPHeaders([(":path", "/"), (":method", "GET"), (":scheme", "https"), (":authority", "localhost")])
         let clientStreamID = HTTP2StreamID()
         var reqFrame = HTTP2Frame(streamID: clientStreamID, payload: .headers(headers))
-        reqFrame.endStream = true
+        reqFrame.flags.insert(.endStream)
 
         let serverStreamID = try self.assertFramesRoundTrip(frames: [reqFrame], sender: self.clientChannel, receiver: self.serverChannel).first!.streamID
 
@@ -1026,7 +1026,7 @@ class SimpleClientServerTests: XCTestCase {
         // Now we send the final response back.
         let responseHeaders = HTTPHeaders([(":status", "200"), ("content-length", "0")])
         var respFrame = HTTP2Frame(streamID: serverStreamID, payload: .headers(responseHeaders))
-        respFrame.endStream = true
+        respFrame.flags.insert(.endStream)
         try self.assertFramesRoundTrip(frames: [respFrame], sender: self.serverChannel, receiver: self.clientChannel)
 
         XCTAssertNoThrow(try self.clientChannel.finish())
@@ -1083,7 +1083,7 @@ class SimpleClientServerTests: XCTestCase {
         let clientStreamID = HTTP2StreamID()
         let reqFrame = HTTP2Frame(streamID: clientStreamID, payload: .headers(headers))
         var reqBodyFrame = HTTP2Frame(streamID: clientStreamID, payload: .data(.byteBuffer(requestBody)))
-        reqBodyFrame.endStream = true
+        reqBodyFrame.flags.insert(.endStream)
 
         let serverStreamID = try self.assertFramesRoundTrip(frames: [reqFrame, reqBodyFrame], sender: self.clientChannel, receiver: self.serverChannel).first!.streamID
 
@@ -1094,7 +1094,7 @@ class SimpleClientServerTests: XCTestCase {
         // Let's send a quick response back.
         let responseHeaders = HTTPHeaders([(":status", "200"), ("content-length", "0")])
         var respFrame = HTTP2Frame(streamID: serverStreamID, payload: .headers(responseHeaders))
-        respFrame.endStream = true
+        respFrame.flags.insert(.endStream)
         try self.assertFramesRoundTrip(frames: [respFrame], sender: self.serverChannel, receiver: self.clientChannel)
 
         // Now the streams are closed, they should have seen user events.
@@ -1250,13 +1250,13 @@ class SimpleClientServerTests: XCTestCase {
             let clientStreamID = HTTP2StreamID()
             let reqFrame = HTTP2Frame(streamID: clientStreamID, payload: .headers(requestHeaders))
             var reqBodyFrame = HTTP2Frame(streamID: clientStreamID, payload: .data(.byteBuffer(requestBody)))
-            reqBodyFrame.endStream = true
+            reqBodyFrame.flags.insert(.endStream)
 
             let serverStreamID = try self.assertFramesRoundTrip(frames: [reqFrame, reqBodyFrame], sender: self.clientChannel, receiver: self.serverChannel).first!.streamID
 
             // Let's send a quick response back.
             var respFrame = HTTP2Frame(streamID: serverStreamID, payload: .headers(responseHeaders))
-            respFrame.endStream = true
+            respFrame.flags.insert(.endStream)
             try self.assertFramesRoundTrip(frames: [respFrame], sender: self.serverChannel, receiver: self.clientChannel)
         }
 
@@ -1284,13 +1284,13 @@ class SimpleClientServerTests: XCTestCase {
             let clientStreamID = HTTP2StreamID()
             let reqFrame = HTTP2Frame(streamID: clientStreamID, payload: .headers(requestHeaders))
             var reqBodyFrame = HTTP2Frame(streamID: clientStreamID, payload: .data(.byteBuffer(requestBody)))
-            reqBodyFrame.endStream = true
+            reqBodyFrame.flags.insert(.endStream)
 
             let serverStreamID = try self.assertFramesRoundTrip(frames: [reqFrame, reqBodyFrame], sender: self.clientChannel, receiver: self.serverChannel).first!.streamID
 
             // Let's send a quick response back.
             var respFrame = HTTP2Frame(streamID: serverStreamID, payload: .headers(responseHeaders))
-            respFrame.endStream = true
+            respFrame.flags.insert(.endStream)
             try self.assertFramesRoundTrip(frames: [respFrame], sender: self.serverChannel, receiver: self.clientChannel)
         }
 
@@ -1309,7 +1309,7 @@ class SimpleClientServerTests: XCTestCase {
         // Clean it up with the server now.
         let serverFrames = serverStreamIDs.map { streamID -> HTTP2Frame in
             var respFrame = HTTP2Frame(streamID: streamID, payload: .headers(responseHeaders))
-            respFrame.endStream = true
+            respFrame.flags.insert(.endStream)
             return respFrame
         }
         try self.assertFramesRoundTrip(frames: serverFrames, sender: self.serverChannel, receiver: self.clientChannel)
diff --git a/Tests/NIOHTTP2Tests/TestUtilities.swift b/Tests/NIOHTTP2Tests/TestUtilities.swift
index 745726d..4ac6f73 100644
--- a/Tests/NIOHTTP2Tests/TestUtilities.swift
+++ b/Tests/NIOHTTP2Tests/TestUtilities.swift
@@ -150,6 +150,10 @@ extension EmbeddedChannel {
 }
 
 extension HTTP2Frame {
+    var ack: Bool {
+        return self.flags.contains(.ack)
+    }
+    
     /// Asserts that the given frame is a SETTINGS frame.
     func assertSettingsFrame(expectedSettings: [HTTP2Setting], ack: Bool, file: StaticString = #file, line: UInt = #line) {
         guard case .settings(let values) = self.payload else {
@@ -196,7 +200,7 @@ extension HTTP2Frame {
         guard case .headers(let payload) = frame.payload else {
             preconditionFailure("Headers frames can never match non-headers frames")
         }
-        self.assertHeadersFrame(endStream: frame.endStream,
+        self.assertHeadersFrame(endStream: frame.flags.contains(.endStream),
                                 streamID: frame.streamID.networkStreamID!,
                                 payload: payload,
                                 file: file,
@@ -211,8 +215,8 @@ extension HTTP2Frame {
             return
         }
 
-        XCTAssertEqual(self.endStream, endStream,
-                       "Unexpected endStream: expected \(endStream), got \(self.endStream)", file: file, line: line)
+        XCTAssertEqual(self.flags.contains(.endStream), endStream,
+                       "Unexpected endStream: expected \(endStream), got \(self.flags.contains(.endStream))", file: file, line: line)
         XCTAssertEqual(self.streamID.networkStreamID!, streamID,
                        "Unexpected streamID: expected \(streamID), got \(self.streamID.networkStreamID!)", file: file, line: line)
         XCTAssertEqual(payload, actualPayload, "Non-equal payloads: expected \(payload), got \(actualPayload)", file: file, line: line)
@@ -231,7 +235,7 @@ extension HTTP2Frame {
             preconditionFailure("Data frames can never match non-data frames")
         }
 
-        self.assertDataFrame(endStream: frame.endStream,
+        self.assertDataFrame(endStream: frame.flags.contains(.endStream),
                              streamID: frame.streamID.networkStreamID!,
                              payload: expectedPayload,
                              file: file,
@@ -245,8 +249,8 @@ extension HTTP2Frame {
             return
         }
 
-        XCTAssertEqual(self.endStream, endStream,
-                       "Unexpected endStream: expected \(endStream), got \(self.endStream)", file: file, line: line)
+        XCTAssertEqual(self.flags.contains(.endStream), endStream,
+                       "Unexpected endStream: expected \(endStream), got \(self.flags.contains(.endStream))", file: file, line: line)
         XCTAssertEqual(self.streamID.networkStreamID!, streamID,
                        "Unexpected streamID: expected \(streamID), got \(self.streamID.networkStreamID!)", file: file, line: line)
         XCTAssertEqual(actualPayload, payload,